Compare commits
No commits in common. "1d8f288d7b33c9b14058cf7d3ab813c50dd58399" and "9afc586cabd8390fd9194fd36661f0ae009f3269" have entirely different histories.
1d8f288d7b
...
9afc586cab
|
|
@ -51,8 +51,8 @@
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "anyComponentStyle",
|
"type": "anyComponentStyle",
|
||||||
"maximumWarning": "35kB",
|
"maximumWarning": "20kB",
|
||||||
"maximumError": "60kB"
|
"maximumError": "45kB"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"outputHashing": "all"
|
"outputHashing": "all"
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -32,7 +32,6 @@
|
||||||
"bootstrap": "^5.3.8",
|
"bootstrap": "^5.3.8",
|
||||||
"bootstrap-icons": "^1.13.1",
|
"bootstrap-icons": "^1.13.1",
|
||||||
"chart.js": "^4.5.1",
|
"chart.js": "^4.5.1",
|
||||||
"exceljs": "^4.4.0",
|
|
||||||
"rxjs": "~7.8.0",
|
"rxjs": "~7.8.0",
|
||||||
"tslib": "^2.3.0",
|
"tslib": "^2.3.0",
|
||||||
"zone.js": "~0.15.0"
|
"zone.js": "~0.15.0"
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,6 @@ import { Faturamento } from './pages/faturamento/faturamento';
|
||||||
|
|
||||||
import { authGuard } from './guards/auth.guard';
|
import { authGuard } from './guards/auth.guard';
|
||||||
import { sysadminOrGestorGuard } from './guards/sysadmin-or-gestor.guard';
|
import { sysadminOrGestorGuard } from './guards/sysadmin-or-gestor.guard';
|
||||||
import { sysadminOrFinanceiroGuard } from './guards/sysadmin-or-financeiro.guard';
|
|
||||||
import { sysadminOnlyGuard } from './guards/sysadmin-only.guard';
|
import { sysadminOnlyGuard } from './guards/sysadmin-only.guard';
|
||||||
import { DadosUsuarios } from './pages/dados-usuarios/dados-usuarios';
|
import { DadosUsuarios } from './pages/dados-usuarios/dados-usuarios';
|
||||||
import { VigenciaComponent } from './pages/vigencia/vigencia';
|
import { VigenciaComponent } from './pages/vigencia/vigencia';
|
||||||
|
|
@ -20,12 +19,8 @@ import { ChipsControleRecebidos } from './pages/chips-controle-recebidos/chips-c
|
||||||
import { Resumo } from './pages/resumo/resumo';
|
import { Resumo } from './pages/resumo/resumo';
|
||||||
import { Parcelamentos } from './pages/parcelamentos/parcelamentos';
|
import { Parcelamentos } from './pages/parcelamentos/parcelamentos';
|
||||||
import { Historico } from './pages/historico/historico';
|
import { Historico } from './pages/historico/historico';
|
||||||
import { HistoricoLinhas } from './pages/historico-linhas/historico-linhas';
|
|
||||||
import { HistoricoChips } from './pages/historico-chips/historico-chips';
|
|
||||||
import { Perfil } from './pages/perfil/perfil';
|
import { Perfil } from './pages/perfil/perfil';
|
||||||
import { SystemProvisionUserPage } from './pages/system-provision-user/system-provision-user';
|
import { SystemProvisionUserPage } from './pages/system-provision-user/system-provision-user';
|
||||||
import { SolicitacoesLinhas } from './pages/solicitacoes-linhas/solicitacoes-linhas';
|
|
||||||
import { MveAuditoriaPage } from './pages/mve-auditoria/mve-auditoria';
|
|
||||||
|
|
||||||
export const routes: Routes = [
|
export const routes: Routes = [
|
||||||
{ path: '', component: Home },
|
{ path: '', component: Home },
|
||||||
|
|
@ -34,19 +29,15 @@ export const routes: Routes = [
|
||||||
|
|
||||||
{ path: 'geral', component: Geral, canActivate: [authGuard], title: 'Geral' },
|
{ path: 'geral', component: Geral, canActivate: [authGuard], title: 'Geral' },
|
||||||
{ path: 'mureg', component: Mureg, canActivate: [authGuard], title: 'Mureg' },
|
{ path: 'mureg', component: Mureg, canActivate: [authGuard], title: 'Mureg' },
|
||||||
{ path: 'faturamento', component: Faturamento, canActivate: [authGuard, sysadminOrFinanceiroGuard], title: 'Faturamento' },
|
{ path: 'faturamento', component: Faturamento, canActivate: [authGuard, sysadminOrGestorGuard], title: 'Faturamento' },
|
||||||
{ path: 'dadosusuarios', component: DadosUsuarios, canActivate: [authGuard], title: 'Dados dos Usuários' },
|
{ path: 'dadosusuarios', component: DadosUsuarios, canActivate: [authGuard], title: 'Dados dos Usuários' },
|
||||||
{ path: 'vigencia', component: VigenciaComponent, canActivate: [authGuard], title: 'Vigência' },
|
{ path: 'vigencia', component: VigenciaComponent, canActivate: [authGuard], title: 'Vigência' },
|
||||||
{ path: 'trocanumero', component: TrocaNumero, canActivate: [authGuard], title: 'Troca de Número' },
|
{ path: 'trocanumero', component: TrocaNumero, canActivate: [authGuard], title: 'Troca de Número' },
|
||||||
{ path: 'notificacoes', component: Notificacoes, canActivate: [authGuard], title: 'Notificações' },
|
{ path: 'notificacoes', component: Notificacoes, canActivate: [authGuard], title: 'Notificações' },
|
||||||
{ path: 'chips-controle-recebidos', component: ChipsControleRecebidos, canActivate: [authGuard, sysadminOrGestorGuard], title: 'Chips Controle Recebidos' },
|
{ path: 'chips-controle-recebidos', component: ChipsControleRecebidos, canActivate: [authGuard, sysadminOrGestorGuard], title: 'Chips Controle Recebidos' },
|
||||||
{ path: 'resumo', component: Resumo, canActivate: [authGuard], title: 'Resumo' },
|
{ path: 'resumo', component: Resumo, canActivate: [authGuard], title: 'Resumo' },
|
||||||
{ path: 'parcelamentos', component: Parcelamentos, canActivate: [authGuard, sysadminOrFinanceiroGuard], title: 'Parcelamentos' },
|
{ path: 'parcelamentos', component: Parcelamentos, canActivate: [authGuard, sysadminOrGestorGuard], title: 'Parcelamentos' },
|
||||||
{ path: 'historico', component: Historico, canActivate: [authGuard, sysadminOrGestorGuard], title: 'Histórico' },
|
{ path: 'historico', component: Historico, canActivate: [authGuard, sysadminOrGestorGuard], title: 'Histórico' },
|
||||||
{ path: 'historico-linhas', component: HistoricoLinhas, canActivate: [authGuard, sysadminOrGestorGuard], title: 'Histórico de Linhas' },
|
|
||||||
{ path: 'historico-chips', component: HistoricoChips, canActivate: [authGuard, sysadminOrGestorGuard], title: 'Histórico de Chips' },
|
|
||||||
{ path: 'solicitacoes', component: SolicitacoesLinhas, canActivate: [authGuard, sysadminOrGestorGuard], title: 'Solicitações' },
|
|
||||||
{ path: 'auditoria-mve', component: MveAuditoriaPage, canActivate: [authGuard, sysadminOrGestorGuard], title: 'Auditoria MVE' },
|
|
||||||
{ path: 'perfil', component: Perfil, canActivate: [authGuard], title: 'Perfil' },
|
{ path: 'perfil', component: Perfil, canActivate: [authGuard], title: 'Perfil' },
|
||||||
{
|
{
|
||||||
path: 'system/fornecer-usuario',
|
path: 'system/fornecer-usuario',
|
||||||
|
|
|
||||||
|
|
@ -29,8 +29,6 @@ export class AppComponent {
|
||||||
// ✅ rotas internas (LOGADO) que devem esconder footer
|
// ✅ rotas internas (LOGADO) que devem esconder footer
|
||||||
private readonly loggedPrefixes = [
|
private readonly loggedPrefixes = [
|
||||||
'/geral',
|
'/geral',
|
||||||
'/auditoria-mve',
|
|
||||||
'/solicitacoes-linhas',
|
|
||||||
'/mureg',
|
'/mureg',
|
||||||
'/faturamento',
|
'/faturamento',
|
||||||
'/dadosusuarios',
|
'/dadosusuarios',
|
||||||
|
|
@ -42,8 +40,6 @@ export class AppComponent {
|
||||||
'/resumo',
|
'/resumo',
|
||||||
'/parcelamentos',
|
'/parcelamentos',
|
||||||
'/historico',
|
'/historico',
|
||||||
'/historico-linhas',
|
|
||||||
'/historico-chips',
|
|
||||||
'/perfil',
|
'/perfil',
|
||||||
'/system',
|
'/system',
|
||||||
];
|
];
|
||||||
|
|
|
||||||
|
|
@ -216,24 +216,22 @@
|
||||||
</ng-container>
|
</ng-container>
|
||||||
|
|
||||||
<ng-template #publicHeader>
|
<ng-template #publicHeader>
|
||||||
<div class="public-header-layout">
|
<a routerLink="/" class="logo-area">
|
||||||
<a routerLink="/" class="logo-area">
|
<img src="linegestao-logo.png" alt="Line Gestão" class="logo-symbol" />
|
||||||
<img src="linegestao-logo.png" alt="Line Gestão" class="logo-symbol" />
|
<div class="lg-wordmark" aria-label="Line Gestão">
|
||||||
<div class="lg-wordmark" aria-label="Line Gestão">
|
<div class="lg-wordmark__line">Line</div>
|
||||||
<div class="lg-wordmark__line">Line</div>
|
<div class="lg-wordmark__movel">Gestão</div>
|
||||||
<div class="lg-wordmark__movel">Gestão</div>
|
|
||||||
</div>
|
|
||||||
</a>
|
|
||||||
<nav class="nav-links">
|
|
||||||
<a href="https://www.linemovel.com.br/empresas" target="_blank" class="nav-link">Para Empresas</a>
|
|
||||||
<a href="https://www.linemovel.com.br/proposta" target="_blank" class="nav-link">Proposta</a>
|
|
||||||
<a href="https://www.linemovel.com.br/sobrenos" target="_blank" class="nav-link">Sobre</a>
|
|
||||||
</nav>
|
|
||||||
<div class="header-actions public-header-actions">
|
|
||||||
<a routerLink="/login" class="btn-login-header">
|
|
||||||
Acessar Sistema <i class="bi bi-arrow-right-short"></i>
|
|
||||||
</a>
|
|
||||||
</div>
|
</div>
|
||||||
|
</a>
|
||||||
|
<nav class="nav-links">
|
||||||
|
<a href="https://www.linemovel.com.br/empresas" target="_blank" class="nav-link">Para Empresas</a>
|
||||||
|
<a href="https://www.linemovel.com.br/proposta" target="_blank" class="nav-link">Proposta</a>
|
||||||
|
<a href="https://www.linemovel.com.br/sobrenos" target="_blank" class="nav-link">Sobre</a>
|
||||||
|
</nav>
|
||||||
|
<div class="header-actions">
|
||||||
|
<a routerLink="/login" class="btn-login-header">
|
||||||
|
Acessar Sistema <i class="bi bi-arrow-right-short"></i>
|
||||||
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</ng-template>
|
</ng-template>
|
||||||
|
|
||||||
|
|
@ -528,7 +526,7 @@
|
||||||
</a>
|
</a>
|
||||||
<button type="button" class="close-btn" (click)="closeMenu()"><i class="bi bi-x-lg"></i></button>
|
<button type="button" class="close-btn" (click)="closeMenu()"><i class="bi bi-x-lg"></i></button>
|
||||||
</div>
|
</div>
|
||||||
<div class="side-menu-body custom-scroll">
|
<div class="side-menu-body">
|
||||||
<a routerLink="/dashboard" routerLinkActive="active" class="side-item" (click)="closeMenu()">
|
<a routerLink="/dashboard" routerLinkActive="active" class="side-item" (click)="closeMenu()">
|
||||||
<i class="bi bi-grid-fill"></i> <span>Dashboard</span>
|
<i class="bi bi-grid-fill"></i> <span>Dashboard</span>
|
||||||
</a>
|
</a>
|
||||||
|
|
@ -538,30 +536,18 @@
|
||||||
<a routerLink="/geral" routerLinkActive="active" class="side-item" (click)="closeMenu()">
|
<a routerLink="/geral" routerLinkActive="active" class="side-item" (click)="closeMenu()">
|
||||||
<i class="bi bi-sim"></i> <span>Geral</span>
|
<i class="bi bi-sim"></i> <span>Geral</span>
|
||||||
</a>
|
</a>
|
||||||
<a *ngIf="canViewMveAudit" routerLink="/auditoria-mve" routerLinkActive="active" class="side-item" (click)="closeMenu()">
|
|
||||||
<i class="bi bi-shield-check"></i> <span>Auditoria MVE</span>
|
|
||||||
</a>
|
|
||||||
<a *ngIf="canViewAll" routerLink="/mureg" routerLinkActive="active" class="side-item" (click)="closeMenu()">
|
<a *ngIf="canViewAll" routerLink="/mureg" routerLinkActive="active" class="side-item" (click)="closeMenu()">
|
||||||
<i class="bi bi-diagram-3-fill"></i> <span>Mureg</span>
|
<i class="bi bi-diagram-3-fill"></i> <span>Mureg</span>
|
||||||
</a>
|
</a>
|
||||||
<a *ngIf="canViewFinancialPages" routerLink="/faturamento" routerLinkActive="active" class="side-item" (click)="closeMenu()">
|
<a *ngIf="canViewAll" routerLink="/faturamento" routerLinkActive="active" class="side-item" (click)="closeMenu()">
|
||||||
<i class="bi bi-receipt"></i> <span>Faturamento</span>
|
<i class="bi bi-receipt"></i> <span>Faturamento</span>
|
||||||
</a>
|
</a>
|
||||||
<a *ngIf="canViewFinancialPages" routerLink="/parcelamentos" routerLinkActive="active" class="side-item" (click)="closeMenu()">
|
<a *ngIf="canViewAll" routerLink="/parcelamentos" routerLinkActive="active" class="side-item" (click)="closeMenu()">
|
||||||
<i class="bi bi-wallet2"></i> <span>Parcelamentos</span>
|
<i class="bi bi-wallet2"></i> <span>Parcelamentos</span>
|
||||||
</a>
|
</a>
|
||||||
<a *ngIf="canViewAll" routerLink="/historico" routerLinkActive="active" class="side-item" (click)="closeMenu()">
|
<a *ngIf="canViewAll" routerLink="/historico" routerLinkActive="active" class="side-item" (click)="closeMenu()">
|
||||||
<i class="bi bi-clock-history"></i> <span>Histórico</span>
|
<i class="bi bi-clock-history"></i> <span>Histórico</span>
|
||||||
</a>
|
</a>
|
||||||
<a *ngIf="canViewAll" routerLink="/historico-linhas" routerLinkActive="active" class="side-item" (click)="closeMenu()">
|
|
||||||
<i class="bi bi-diagram-3"></i> <span>Histórico de Linhas</span>
|
|
||||||
</a>
|
|
||||||
<a *ngIf="canViewAll" routerLink="/historico-chips" routerLinkActive="active" class="side-item" (click)="closeMenu()">
|
|
||||||
<i class="bi bi-sim"></i> <span>Histórico de Chips</span>
|
|
||||||
</a>
|
|
||||||
<a *ngIf="canViewAll" routerLink="/solicitacoes" routerLinkActive="active" class="side-item" (click)="closeMenu()">
|
|
||||||
<i class="bi bi-envelope-paper"></i> <span>Solicitações</span>
|
|
||||||
</a>
|
|
||||||
<a *ngIf="canViewAll" routerLink="/dadosusuarios" routerLinkActive="active" class="side-item" (click)="closeMenu()">
|
<a *ngIf="canViewAll" routerLink="/dadosusuarios" routerLinkActive="active" class="side-item" (click)="closeMenu()">
|
||||||
<i class="bi bi-people-fill"></i> <span>Dados PF/PJ</span>
|
<i class="bi bi-people-fill"></i> <span>Dados PF/PJ</span>
|
||||||
</a>
|
</a>
|
||||||
|
|
|
||||||
|
|
@ -196,27 +196,6 @@ $logo-secondary-grey: #757575;
|
||||||
display: inline-flex; align-items: center; gap: 6px; color: $text-main; text-decoration: none; font-weight: 600; font-size: 14px; transition: color 0.2s;
|
display: inline-flex; align-items: center; gap: 6px; color: $text-main; text-decoration: none; font-weight: 600; font-size: 14px; transition: color 0.2s;
|
||||||
&:hover { color: $primary; }
|
&:hover { color: $primary; }
|
||||||
}
|
}
|
||||||
.public-header-layout {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 24px;
|
|
||||||
width: 100%;
|
|
||||||
min-width: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.public-header-layout > .logo-area {
|
|
||||||
min-width: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.public-header-layout > .nav-links {
|
|
||||||
flex: 1 1 auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.public-header-actions {
|
|
||||||
margin-left: auto;
|
|
||||||
flex: 0 0 auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.header-actions { display: flex; align-items: center; margin-left: auto; justify-content: flex-end; flex: 0 0 auto; }
|
.header-actions { display: flex; align-items: center; margin-left: auto; justify-content: flex-end; flex: 0 0 auto; }
|
||||||
.btn-login-header {
|
.btn-login-header {
|
||||||
display: inline-flex; align-items: center; gap: 6px; padding: 8px 16px; border-radius: 99px;
|
display: inline-flex; align-items: center; gap: 6px; padding: 8px 16px; border-radius: 99px;
|
||||||
|
|
@ -940,17 +919,7 @@ $logo-secondary-grey: #757575;
|
||||||
.side-wordmark__movel {
|
.side-wordmark__movel {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
.side-menu-body {
|
.side-menu-body { padding: 16px; display: flex; flex-direction: column; gap: 4px; }
|
||||||
flex: 1 1 auto;
|
|
||||||
min-height: 0;
|
|
||||||
padding: 16px;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 4px;
|
|
||||||
overflow-y: auto;
|
|
||||||
overflow-x: hidden;
|
|
||||||
overscroll-behavior: contain;
|
|
||||||
}
|
|
||||||
.side-item {
|
.side-item {
|
||||||
padding: 10px 12px; border-radius: 8px; color: $text-main; text-decoration: none; font-size: 14px; font-weight: 500; display: flex; align-items: center; gap: 10px;
|
padding: 10px 12px; border-radius: 8px; color: $text-main; text-decoration: none; font-size: 14px; font-weight: 500; display: flex; align-items: center; gap: 10px;
|
||||||
&:hover { background: $bg-light; }
|
&:hover { background: $bg-light; }
|
||||||
|
|
@ -1103,35 +1072,28 @@ $logo-secondary-grey: #757575;
|
||||||
--scale: 0.21;
|
--scale: 0.21;
|
||||||
}
|
}
|
||||||
|
|
||||||
.public-header-layout {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: minmax(0, 1fr) auto;
|
|
||||||
align-items: center;
|
|
||||||
gap: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Header público (Home/Login/Register): mantém logo visível e CTA fixo à direita */
|
/* Header público (Home/Login/Register): mantém logo visível e CTA fixo à direita */
|
||||||
.public-header-layout > .logo-area {
|
.header-inner > .logo-area {
|
||||||
flex: 1 1 auto;
|
flex: 1 1 auto;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.public-header-layout > .logo-area .lg-wordmark {
|
.header-inner > .logo-area .lg-wordmark {
|
||||||
display: block;
|
display: block;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.public-header-layout > .logo-area .lg-wordmark {
|
.header-inner > .logo-area .lg-wordmark {
|
||||||
--scale: 0.19;
|
--scale: 0.19;
|
||||||
}
|
}
|
||||||
|
|
||||||
.public-header-layout > .header-actions {
|
.header-inner > .header-actions {
|
||||||
margin-left: 0;
|
margin-left: auto;
|
||||||
flex: 0 0 auto;
|
flex: 0 0 auto;
|
||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
}
|
}
|
||||||
|
|
||||||
.public-header-layout > .header-actions .btn-login-header {
|
.header-inner > .header-actions .btn-login-header {
|
||||||
padding: 7px 10px;
|
padding: 7px 10px;
|
||||||
gap: 4px;
|
gap: 4px;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
|
|
@ -1442,20 +1404,20 @@ $logo-secondary-grey: #757575;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 420px) {
|
@media (max-width: 420px) {
|
||||||
.public-header-layout > .logo-area {
|
.header-inner > .logo-area {
|
||||||
gap: 5px;
|
gap: 5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.public-header-layout > .logo-area .logo-symbol {
|
.header-inner > .logo-area .logo-symbol {
|
||||||
width: 32px;
|
width: 32px;
|
||||||
height: 32px;
|
height: 32px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.public-header-layout > .logo-area .lg-wordmark {
|
.header-inner > .logo-area .lg-wordmark {
|
||||||
--scale: 0.18;
|
--scale: 0.18;
|
||||||
}
|
}
|
||||||
|
|
||||||
.public-header-layout > .header-actions .btn-login-header {
|
.header-inner > .header-actions .btn-login-header {
|
||||||
padding: 6px 8px;
|
padding: 6px 8px;
|
||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
gap: 3px;
|
gap: 3px;
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,6 @@ import { CustomSelectComponent } from '../custom-select/custom-select';
|
||||||
import { Subscription } from 'rxjs';
|
import { Subscription } from 'rxjs';
|
||||||
import { environment } from '../../../environments/environment';
|
import { environment } from '../../../environments/environment';
|
||||||
import { confirmActionModal, confirmDeletionWithTyping, showDeletionWarning } from '../../utils/destructive-confirmation';
|
import { confirmActionModal, confirmDeletionWithTyping, showDeletionWarning } from '../../utils/destructive-confirmation';
|
||||||
import { buildApiBaseUrl } from '../../utils/api-base.util';
|
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-header',
|
selector: 'app-header',
|
||||||
|
|
@ -35,11 +34,7 @@ export class Header implements AfterViewInit, OnDestroy {
|
||||||
isLoggedHeader = false;
|
isLoggedHeader = false;
|
||||||
isHome = false;
|
isHome = false;
|
||||||
isSysAdmin = false;
|
isSysAdmin = false;
|
||||||
isGestor = false;
|
|
||||||
isFinanceiro = false;
|
|
||||||
canViewAll = false;
|
canViewAll = false;
|
||||||
canViewFinancialPages = false;
|
|
||||||
canViewMveAudit = false;
|
|
||||||
clientTenantDisplayName = '';
|
clientTenantDisplayName = '';
|
||||||
private clientTenantNameTenantId: string | null = null;
|
private clientTenantNameTenantId: string | null = null;
|
||||||
private readonly baseApi: string;
|
private readonly baseApi: string;
|
||||||
|
|
@ -63,11 +58,10 @@ export class Header implements AfterViewInit, OnDestroy {
|
||||||
createUserForbidden = false;
|
createUserForbidden = false;
|
||||||
createUserSuccess = '';
|
createUserSuccess = '';
|
||||||
readonly permissionOptions = [
|
readonly permissionOptions = [
|
||||||
{ value: 'sysadmin', label: 'SysAdmin' },
|
{ value: 'sysadmin', label: 'SysAdmin' },
|
||||||
{ value: 'gestor', label: 'Gestor' },
|
{ value: 'gestor', label: 'Gestor' },
|
||||||
{ value: 'financeiro', label: 'Financeiro' },
|
{ value: 'cliente', label: 'Cliente' },
|
||||||
{ value: 'cliente', label: 'Cliente' },
|
];
|
||||||
];
|
|
||||||
|
|
||||||
manageUsersLoading = false;
|
manageUsersLoading = false;
|
||||||
manageUsersErrors: ApiFieldError[] = [];
|
manageUsersErrors: ApiFieldError[] = [];
|
||||||
|
|
@ -99,10 +93,6 @@ export class Header implements AfterViewInit, OnDestroy {
|
||||||
'/resumo',
|
'/resumo',
|
||||||
'/parcelamentos',
|
'/parcelamentos',
|
||||||
'/historico',
|
'/historico',
|
||||||
'/historico-linhas',
|
|
||||||
'/historico-chips',
|
|
||||||
'/solicitacoes',
|
|
||||||
'/auditoria-mve',
|
|
||||||
'/perfil',
|
'/perfil',
|
||||||
'/system',
|
'/system',
|
||||||
];
|
];
|
||||||
|
|
@ -117,7 +107,8 @@ export class Header implements AfterViewInit, OnDestroy {
|
||||||
private hostElement: ElementRef<HTMLElement>,
|
private hostElement: ElementRef<HTMLElement>,
|
||||||
@Inject(PLATFORM_ID) private platformId: object
|
@Inject(PLATFORM_ID) private platformId: object
|
||||||
) {
|
) {
|
||||||
this.baseApi = buildApiBaseUrl(environment.apiUrl);
|
const raw = (environment.apiUrl || '').replace(/\/+$/, '');
|
||||||
|
this.baseApi = raw.toLowerCase().endsWith('/api') ? raw : `${raw}/api`;
|
||||||
|
|
||||||
this.createUserForm = this.fb.group(
|
this.createUserForm = this.fb.group(
|
||||||
{
|
{
|
||||||
|
|
@ -222,23 +213,15 @@ export class Header implements AfterViewInit, OnDestroy {
|
||||||
private syncPermissions() {
|
private syncPermissions() {
|
||||||
if (!isPlatformBrowser(this.platformId)) {
|
if (!isPlatformBrowser(this.platformId)) {
|
||||||
this.isSysAdmin = false;
|
this.isSysAdmin = false;
|
||||||
this.isGestor = false;
|
|
||||||
this.isFinanceiro = false;
|
|
||||||
this.canViewAll = false;
|
this.canViewAll = false;
|
||||||
this.canViewFinancialPages = false;
|
|
||||||
this.clientTenantDisplayName = '';
|
this.clientTenantDisplayName = '';
|
||||||
this.clientTenantNameTenantId = null;
|
this.clientTenantNameTenantId = null;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const isSysAdmin = this.authService.hasRole('sysadmin');
|
const isSysAdmin = this.authService.hasRole('sysadmin');
|
||||||
const isGestor = this.authService.hasRole('gestor');
|
const isGestor = this.authService.hasRole('gestor');
|
||||||
const isFinanceiro = this.authService.hasRole('financeiro');
|
|
||||||
this.isSysAdmin = isSysAdmin;
|
this.isSysAdmin = isSysAdmin;
|
||||||
this.isGestor = isGestor;
|
this.canViewAll = isSysAdmin || isGestor;
|
||||||
this.isFinanceiro = isFinanceiro;
|
|
||||||
this.canViewAll = isSysAdmin || isGestor || isFinanceiro;
|
|
||||||
this.canViewFinancialPages = isSysAdmin || isFinanceiro;
|
|
||||||
this.canViewMveAudit = isSysAdmin || isGestor;
|
|
||||||
|
|
||||||
if (!this.isClientHeader) {
|
if (!this.isClientHeader) {
|
||||||
this.clientTenantDisplayName = '';
|
this.clientTenantDisplayName = '';
|
||||||
|
|
@ -514,10 +497,7 @@ export class Header implements AfterViewInit, OnDestroy {
|
||||||
this.optionsOpen = false;
|
this.optionsOpen = false;
|
||||||
this.notificationsOpen = false;
|
this.notificationsOpen = false;
|
||||||
this.isSysAdmin = false;
|
this.isSysAdmin = false;
|
||||||
this.isGestor = false;
|
|
||||||
this.isFinanceiro = false;
|
|
||||||
this.canViewAll = false;
|
this.canViewAll = false;
|
||||||
this.canViewFinancialPages = false;
|
|
||||||
this.router.navigate(['/']);
|
this.router.navigate(['/']);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,14 +0,0 @@
|
||||||
<ng-container *ngIf="open">
|
|
||||||
<div
|
|
||||||
*ngIf="showBackdrop"
|
|
||||||
[ngClass]="backdropClass"
|
|
||||||
(click)="onOverlayClick()"
|
|
||||||
></div>
|
|
||||||
|
|
||||||
<div
|
|
||||||
[ngClass]="overlayClass"
|
|
||||||
(click)="onOverlayClick()"
|
|
||||||
>
|
|
||||||
<ng-content></ng-content>
|
|
||||||
</div>
|
|
||||||
</ng-container>
|
|
||||||
|
|
@ -1,35 +0,0 @@
|
||||||
.modal-backdrop-custom {
|
|
||||||
position: fixed;
|
|
||||||
inset: 0;
|
|
||||||
background: rgba(0, 0, 0, 0.45);
|
|
||||||
z-index: 9990;
|
|
||||||
backdrop-filter: blur(4px);
|
|
||||||
}
|
|
||||||
|
|
||||||
.modal-custom {
|
|
||||||
position: fixed;
|
|
||||||
inset: 0;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
z-index: 9995;
|
|
||||||
padding: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.lg-modal {
|
|
||||||
position: fixed;
|
|
||||||
inset: 0;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
z-index: 9995;
|
|
||||||
padding: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.lg-backdrop {
|
|
||||||
position: fixed;
|
|
||||||
inset: 0;
|
|
||||||
background: rgba(0, 0, 0, 0.45);
|
|
||||||
z-index: 9990;
|
|
||||||
backdrop-filter: blur(2px);
|
|
||||||
}
|
|
||||||
|
|
@ -1,24 +0,0 @@
|
||||||
import { CommonModule } from '@angular/common';
|
|
||||||
import { Component, EventEmitter, Input, Output } from '@angular/core';
|
|
||||||
|
|
||||||
@Component({
|
|
||||||
selector: 'app-modal-layer',
|
|
||||||
standalone: true,
|
|
||||||
imports: [CommonModule],
|
|
||||||
templateUrl: './modal-layer.html',
|
|
||||||
styleUrls: ['./modal-layer.scss'],
|
|
||||||
})
|
|
||||||
export class ModalLayerComponent {
|
|
||||||
@Input() open = false;
|
|
||||||
@Input() showBackdrop = true;
|
|
||||||
@Input() closeOnOverlay = true;
|
|
||||||
@Input() backdropClass = 'modal-backdrop-custom';
|
|
||||||
@Input() overlayClass = 'modal-custom';
|
|
||||||
|
|
||||||
@Output() close = new EventEmitter<void>();
|
|
||||||
|
|
||||||
onOverlayClick(): void {
|
|
||||||
if (!this.closeOnOverlay) return;
|
|
||||||
this.close.emit();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,370 +0,0 @@
|
||||||
<app-modal-layer
|
|
||||||
[open]="chipDetailOpen || controleDetailOpen || chipEditOpen || chipDeleteOpen || controleEditOpen || controleDeleteOpen || chipCreateOpen || controleCreateOpen"
|
|
||||||
(close)="closeChipDetail(); closeControleDetail(); closeChipEdit(); cancelChipDelete(); closeControleEdit(); cancelControleDelete(); closeChipCreate(); closeControleCreate()"
|
|
||||||
>
|
|
||||||
|
|
||||||
<!-- MODAL CHIP -->
|
|
||||||
<ng-container *ngIf="chipDetailOpen">
|
|
||||||
<div class="modal-card modal-lg" (click)="$event.stopPropagation()">
|
|
||||||
<div class="modal-header">
|
|
||||||
<div class="modal-title">
|
|
||||||
<span class="icon-bg primary-soft"><i class="bi bi-sim"></i></span>
|
|
||||||
Detalhes do Chip
|
|
||||||
</div>
|
|
||||||
<button class="btn btn-sm btn-icon" (click)="closeChipDetail()"><i class="bi bi-x-lg"></i></button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="modal-body modern-body bg-light-gray">
|
|
||||||
<div class="p-5 text-center text-muted" *ngIf="chipDetailLoading">
|
|
||||||
<span class="spinner-border me-2"></span> Carregando detalhes...
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="details-dashboard" *ngIf="!chipDetailLoading && chipDetailData">
|
|
||||||
<div class="detail-box w-100">
|
|
||||||
<div class="box-header justify-content-center">
|
|
||||||
<span><i class="bi bi-card-text me-2"></i> Informações do Chip</span>
|
|
||||||
</div>
|
|
||||||
<div class="box-body">
|
|
||||||
<div class="info-grid">
|
|
||||||
<div class="info-item">
|
|
||||||
<span class="lbl">Item</span>
|
|
||||||
<span class="val">{{ display(chipDetailData.item) }}</span>
|
|
||||||
</div>
|
|
||||||
<div class="info-item span-2">
|
|
||||||
<span class="lbl">Número do Chip</span>
|
|
||||||
<span class="val text-brand font-monospace">{{ display(chipDetailData.numeroDoChip) }}</span>
|
|
||||||
</div>
|
|
||||||
<div class="info-item span-2">
|
|
||||||
<span class="lbl">Observações</span>
|
|
||||||
<span class="val">{{ display(chipDetailData.observacoes) }}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</ng-container>
|
|
||||||
|
|
||||||
<!-- MODAL CONTROLE -->
|
|
||||||
<ng-container *ngIf="controleDetailOpen">
|
|
||||||
<div class="modal-card modal-xl-custom" (click)="$event.stopPropagation()">
|
|
||||||
<div class="modal-header">
|
|
||||||
<div class="modal-title">
|
|
||||||
<span class="icon-bg primary-soft"><i class="bi bi-clipboard-data"></i></span>
|
|
||||||
Detalhes do Recebimento
|
|
||||||
</div>
|
|
||||||
<button class="btn btn-sm btn-icon" (click)="closeControleDetail()"><i class="bi bi-x-lg"></i></button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="modal-body modern-body bg-light-gray">
|
|
||||||
<div class="p-5 text-center text-muted" *ngIf="controleDetailLoading">
|
|
||||||
<span class="spinner-border me-2"></span> Carregando detalhes...
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="details-dashboard" *ngIf="!controleDetailLoading && controleDetailData">
|
|
||||||
<div class="detail-box w-100">
|
|
||||||
<div class="box-header justify-content-center">
|
|
||||||
<span><i class="bi bi-card-text me-2"></i> Informações da NF</span>
|
|
||||||
</div>
|
|
||||||
<div class="box-body">
|
|
||||||
<div class="info-grid">
|
|
||||||
<div class="info-item">
|
|
||||||
<span class="lbl">Ano</span>
|
|
||||||
<span class="val">{{ display(controleDetailData.ano) }}</span>
|
|
||||||
</div>
|
|
||||||
<div class="info-item">
|
|
||||||
<span class="lbl">Item</span>
|
|
||||||
<span class="val">{{ display(controleDetailData.item) }}</span>
|
|
||||||
</div>
|
|
||||||
<div class="info-item span-2">
|
|
||||||
<span class="lbl">Nota Fiscal</span>
|
|
||||||
<span class="val">{{ display(controleDetailData.notaFiscal) }}</span>
|
|
||||||
</div>
|
|
||||||
<div class="info-item span-2">
|
|
||||||
<span class="lbl">Chip</span>
|
|
||||||
<span class="val font-monospace">{{ display(controleDetailData.chip) }}</span>
|
|
||||||
</div>
|
|
||||||
<div class="info-item span-2">
|
|
||||||
<span class="lbl">Serial</span>
|
|
||||||
<span class="val font-monospace">{{ display(controleDetailData.serial) }}</span>
|
|
||||||
</div>
|
|
||||||
<div class="info-item span-2">
|
|
||||||
<span class="lbl">Conteúdo da NF</span>
|
|
||||||
<span class="val">{{ display(controleDetailData.conteudoDaNf) }}</span>
|
|
||||||
</div>
|
|
||||||
<div class="info-item">
|
|
||||||
<span class="lbl">Número da Linha</span>
|
|
||||||
<span class="val font-monospace">{{ display(controleDetailData.numeroDaLinha) }}</span>
|
|
||||||
</div>
|
|
||||||
<div class="info-item">
|
|
||||||
<span class="lbl">Quantidade</span>
|
|
||||||
<span class="val">{{ display(controleDetailData.quantidade) }}</span>
|
|
||||||
</div>
|
|
||||||
<div class="info-item">
|
|
||||||
<span class="lbl">Valor Unit</span>
|
|
||||||
<span class="val">{{ formatMoney(controleDetailData.valorUnit) }}</span>
|
|
||||||
</div>
|
|
||||||
<div class="info-item">
|
|
||||||
<span class="lbl">Valor da NF</span>
|
|
||||||
<span class="val text-brand">{{ formatMoney(controleDetailData.valorDaNf) }}</span>
|
|
||||||
</div>
|
|
||||||
<div class="info-item">
|
|
||||||
<span class="lbl">Data da NF</span>
|
|
||||||
<span class="val">{{ formatDate(controleDetailData.dataDaNf) }}</span>
|
|
||||||
</div>
|
|
||||||
<div class="info-item">
|
|
||||||
<span class="lbl">Recebimento</span>
|
|
||||||
<span class="val">{{ formatDate(controleDetailData.dataDoRecebimento) }}</span>
|
|
||||||
</div>
|
|
||||||
<div class="info-item">
|
|
||||||
<span class="lbl">Tipo</span>
|
|
||||||
<span class="val">{{ isResumo(controleDetailData) ? "RESUMO" : "DETALHE" }}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</ng-container>
|
|
||||||
|
|
||||||
<!-- MODAL CHIP CREATE -->
|
|
||||||
<ng-container *ngIf="chipCreateOpen">
|
|
||||||
<div class="modal-card modal-lg create-modal" (click)="$event.stopPropagation()">
|
|
||||||
<div class="modal-header">
|
|
||||||
<div class="modal-title">
|
|
||||||
<span class="icon-bg primary-soft"><i class="bi bi-plus-circle"></i></span>
|
|
||||||
Novo Chip
|
|
||||||
</div>
|
|
||||||
<button class="btn btn-sm btn-icon" (click)="closeChipCreate()"><i class="bi bi-x-lg"></i></button>
|
|
||||||
</div>
|
|
||||||
<div class="modal-body modern-body bg-light-gray" *ngIf="chipCreateModel">
|
|
||||||
<div class="edit-sections">
|
|
||||||
<details open class="detail-box">
|
|
||||||
<summary class="box-header">
|
|
||||||
<span><i class="bi bi-sim me-2"></i> Informações do Chip</span>
|
|
||||||
<i class="bi bi-chevron-down ms-auto transition-icon"></i>
|
|
||||||
</summary>
|
|
||||||
<div class="box-body">
|
|
||||||
<div class="form-grid">
|
|
||||||
<div class="form-field"><label>Item (opcional)</label><input class="form-control form-control-sm" type="number" [(ngModel)]="chipCreateModel.item" /></div>
|
|
||||||
<div class="form-field span-2"><label>Número do Chip</label><input class="form-control form-control-sm" [(ngModel)]="chipCreateModel.numeroDoChip" /></div>
|
|
||||||
<div class="form-field span-2"><label>Observações</label><input class="form-control form-control-sm" [(ngModel)]="chipCreateModel.observacoes" /></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</details>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="modal-footer p-3 text-end border-top bg-white">
|
|
||||||
<button class="btn btn-glass btn-sm me-2" (click)="closeChipCreate()">Cancelar</button>
|
|
||||||
<button class="btn btn-brand btn-sm" [disabled]="chipCreateSaving" (click)="saveChipCreate()">
|
|
||||||
{{ chipCreateSaving ? 'Salvando...' : 'Salvar' }}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</ng-container>
|
|
||||||
|
|
||||||
<!-- MODAL CONTROLE CREATE -->
|
|
||||||
<ng-container *ngIf="controleCreateOpen">
|
|
||||||
<div class="modal-card modal-xl-custom create-modal" (click)="$event.stopPropagation()">
|
|
||||||
<div class="modal-header">
|
|
||||||
<div class="modal-title">
|
|
||||||
<span class="icon-bg primary-soft"><i class="bi bi-plus-circle"></i></span>
|
|
||||||
Novo Recebimento
|
|
||||||
</div>
|
|
||||||
<button class="btn btn-sm btn-icon" (click)="closeControleCreate()"><i class="bi bi-x-lg"></i></button>
|
|
||||||
</div>
|
|
||||||
<div class="modal-body modern-body bg-light-gray" *ngIf="controleCreateModel">
|
|
||||||
<div class="edit-sections">
|
|
||||||
<details open class="detail-box">
|
|
||||||
<summary class="box-header">
|
|
||||||
<span><i class="bi bi-card-list me-2"></i> Dados da Nota</span>
|
|
||||||
<i class="bi bi-chevron-down ms-auto transition-icon"></i>
|
|
||||||
</summary>
|
|
||||||
<div class="box-body">
|
|
||||||
<div class="form-grid">
|
|
||||||
<div class="form-field"><label>Ano</label><input class="form-control form-control-sm" type="number" [(ngModel)]="controleCreateModel.ano" /></div>
|
|
||||||
<div class="form-field"><label>Item</label><input class="form-control form-control-sm" type="number" [(ngModel)]="controleCreateModel.item" /></div>
|
|
||||||
<div class="form-field span-2"><label>Nota Fiscal</label><input class="form-control form-control-sm" [(ngModel)]="controleCreateModel.notaFiscal" /></div>
|
|
||||||
<div class="form-field span-2"><label>Conteúdo da NF</label><input class="form-control form-control-sm" [(ngModel)]="controleCreateModel.conteudoDaNf" /></div>
|
|
||||||
<div class="form-field"><label>Serial</label><input class="form-control form-control-sm" [(ngModel)]="controleCreateModel.serial" /></div>
|
|
||||||
<div class="form-field"><label>Chip</label><input class="form-control form-control-sm" [(ngModel)]="controleCreateModel.chip" /></div>
|
|
||||||
<div class="form-field"><label>Número da Linha</label><input class="form-control form-control-sm" [(ngModel)]="controleCreateModel.numeroDaLinha" /></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</details>
|
|
||||||
|
|
||||||
<details open class="detail-box">
|
|
||||||
<summary class="box-header">
|
|
||||||
<span><i class="bi bi-currency-exchange me-2"></i> Valores e Datas</span>
|
|
||||||
<i class="bi bi-chevron-down ms-auto transition-icon"></i>
|
|
||||||
</summary>
|
|
||||||
<div class="box-body">
|
|
||||||
<div class="form-grid">
|
|
||||||
<div class="form-field"><label>Valor Unit</label><input class="form-control form-control-sm" type="number" [(ngModel)]="controleCreateModel.valorUnit" (ngModelChange)="onControleCreateValueChange()" /></div>
|
|
||||||
<div class="form-field"><label>Quantidade</label><input class="form-control form-control-sm" type="number" [(ngModel)]="controleCreateModel.quantidade" (ngModelChange)="onControleCreateValueChange()" /></div>
|
|
||||||
<div class="form-field"><label>Valor da NF</label><input class="form-control form-control-sm" type="number" [(ngModel)]="controleCreateModel.valorDaNf" (ngModelChange)="onControleCreateValueChange()" /></div>
|
|
||||||
<div class="form-field"><label>Data da NF</label><input class="form-control form-control-sm" type="date" [(ngModel)]="controleCreateDataNf" (ngModelChange)="onControleCreateDateChange()" /></div>
|
|
||||||
<div class="form-field"><label>Recebimento</label><input class="form-control form-control-sm" type="date" [(ngModel)]="controleCreateRecebimento" /></div>
|
|
||||||
<div class="form-field"><label>Resumo</label><input class="form-check-input ms-2" type="checkbox" [(ngModel)]="controleCreateModel.isResumo" /></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</details>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="modal-footer p-3 text-end border-top bg-white">
|
|
||||||
<button class="btn btn-glass btn-sm me-2" (click)="closeControleCreate()">Cancelar</button>
|
|
||||||
<button class="btn btn-brand btn-sm" [disabled]="controleCreateSaving" (click)="saveControleCreate()">
|
|
||||||
{{ controleCreateSaving ? 'Salvando...' : 'Salvar' }}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</ng-container>
|
|
||||||
|
|
||||||
<!-- MODAL CHIP EDIT -->
|
|
||||||
<ng-container *ngIf="chipEditOpen">
|
|
||||||
<div class="modal-card modal-lg" (click)="$event.stopPropagation()">
|
|
||||||
<div class="modal-header">
|
|
||||||
<div class="modal-title">
|
|
||||||
<span class="icon-bg primary-soft"><i class="bi bi-pencil-square"></i></span>
|
|
||||||
Editar Chip
|
|
||||||
</div>
|
|
||||||
<button class="btn btn-sm btn-icon" (click)="closeChipEdit()"><i class="bi bi-x-lg"></i></button>
|
|
||||||
</div>
|
|
||||||
<div class="modal-body modern-body bg-light-gray" *ngIf="chipEditModel">
|
|
||||||
<div class="edit-sections">
|
|
||||||
<details open class="detail-box">
|
|
||||||
<summary class="box-header">
|
|
||||||
<span><i class="bi bi-sim me-2"></i> Identificação do Chip</span>
|
|
||||||
<i class="bi bi-chevron-down ms-auto transition-icon"></i>
|
|
||||||
</summary>
|
|
||||||
<div class="box-body">
|
|
||||||
<div class="form-grid">
|
|
||||||
<div class="form-field"><label>Item</label><input class="form-control form-control-sm" type="number" [(ngModel)]="chipEditModel.item" /></div>
|
|
||||||
<div class="form-field span-2"><label>Número do Chip</label><input class="form-control form-control-sm" [(ngModel)]="chipEditModel.numeroDoChip" /></div>
|
|
||||||
<div class="form-field span-2"><label>Observações</label><input class="form-control form-control-sm" [(ngModel)]="chipEditModel.observacoes" /></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</details>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="modal-footer p-3 text-end border-top bg-white">
|
|
||||||
<button class="btn btn-glass btn-sm me-2" (click)="closeChipEdit()">Cancelar</button>
|
|
||||||
<button class="btn btn-primary btn-sm" [disabled]="chipEditSaving" (click)="saveChipEdit()">
|
|
||||||
{{ chipEditSaving ? 'Salvando...' : 'Salvar' }}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</ng-container>
|
|
||||||
|
|
||||||
<!-- MODAL CHIP DELETE -->
|
|
||||||
<ng-container *ngIf="chipDeleteOpen">
|
|
||||||
<div class="modal-card modal-lg" (click)="$event.stopPropagation()">
|
|
||||||
<div class="modal-header">
|
|
||||||
<div class="modal-title">
|
|
||||||
<span class="icon-bg danger-soft"><i class="bi bi-trash"></i></span>
|
|
||||||
Remover Chip
|
|
||||||
</div>
|
|
||||||
<button class="btn btn-sm btn-icon" (click)="cancelChipDelete()"><i class="bi bi-x-lg"></i></button>
|
|
||||||
</div>
|
|
||||||
<div class="modal-body modern-body bg-light-gray">
|
|
||||||
<div class="confirm-delete">
|
|
||||||
<div class="confirm-icon"><i class="bi bi-trash"></i></div>
|
|
||||||
<p class="mb-0">Confirma remover o chip <strong>{{ chipDeleteTarget?.numeroDoChip }}</strong>?</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="modal-footer p-3 text-end border-top bg-white">
|
|
||||||
<button class="btn btn-glass btn-sm me-2" (click)="cancelChipDelete()">Cancelar</button>
|
|
||||||
<button class="btn btn-danger btn-sm" (click)="confirmChipDelete()">Excluir</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</ng-container>
|
|
||||||
|
|
||||||
<!-- MODAL CONTROLE EDIT -->
|
|
||||||
<ng-container *ngIf="controleEditOpen">
|
|
||||||
<div class="modal-card modal-xl-custom" (click)="$event.stopPropagation()">
|
|
||||||
<div class="modal-header">
|
|
||||||
<div class="modal-title">
|
|
||||||
<span class="icon-bg primary-soft"><i class="bi bi-pencil-square"></i></span>
|
|
||||||
Editar Recebimento
|
|
||||||
</div>
|
|
||||||
<button class="btn btn-sm btn-icon" (click)="closeControleEdit()"><i class="bi bi-x-lg"></i></button>
|
|
||||||
</div>
|
|
||||||
<div class="modal-body modern-body bg-light-gray" *ngIf="controleEditModel">
|
|
||||||
<div class="edit-sections">
|
|
||||||
<details open class="detail-box">
|
|
||||||
<summary class="box-header">
|
|
||||||
<span><i class="bi bi-receipt-cutoff me-2"></i> Documento</span>
|
|
||||||
<i class="bi bi-chevron-down ms-auto transition-icon"></i>
|
|
||||||
</summary>
|
|
||||||
<div class="box-body">
|
|
||||||
<div class="form-grid">
|
|
||||||
<div class="form-field"><label>Ano</label><input class="form-control form-control-sm" type="number" [(ngModel)]="controleEditModel.ano" /></div>
|
|
||||||
<div class="form-field"><label>Item</label><input class="form-control form-control-sm" type="number" [(ngModel)]="controleEditModel.item" /></div>
|
|
||||||
<div class="form-field span-2"><label>Nota Fiscal</label><input class="form-control form-control-sm" [(ngModel)]="controleEditModel.notaFiscal" /></div>
|
|
||||||
<div class="form-field"><label>Chip</label><input class="form-control form-control-sm" [(ngModel)]="controleEditModel.chip" /></div>
|
|
||||||
<div class="form-field"><label>Serial</label><input class="form-control form-control-sm" [(ngModel)]="controleEditModel.serial" /></div>
|
|
||||||
<div class="form-field span-2"><label>Conteúdo da NF</label><input class="form-control form-control-sm" [(ngModel)]="controleEditModel.conteudoDaNf" /></div>
|
|
||||||
<div class="form-field"><label>Número da Linha</label><input class="form-control form-control-sm" [(ngModel)]="controleEditModel.numeroDaLinha" /></div>
|
|
||||||
<div class="form-field"><label>Tipo</label>
|
|
||||||
<select class="form-control form-control-sm" [(ngModel)]="controleEditModel.isResumo">
|
|
||||||
<option [ngValue]="false">DETALHE</option>
|
|
||||||
<option [ngValue]="true">RESUMO</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</details>
|
|
||||||
|
|
||||||
<details open class="detail-box">
|
|
||||||
<summary class="box-header">
|
|
||||||
<span><i class="bi bi-cash-coin me-2"></i> Valores e Datas</span>
|
|
||||||
<i class="bi bi-chevron-down ms-auto transition-icon"></i>
|
|
||||||
</summary>
|
|
||||||
<div class="box-body">
|
|
||||||
<div class="form-grid">
|
|
||||||
<div class="form-field"><label>Quantidade</label><input class="form-control form-control-sm" type="number" [(ngModel)]="controleEditModel.quantidade" (ngModelChange)="onControleEditValueChange()" /></div>
|
|
||||||
<div class="form-field"><label>Valor Unit</label><input class="form-control form-control-sm" type="number" [(ngModel)]="controleEditModel.valorUnit" (ngModelChange)="onControleEditValueChange()" /></div>
|
|
||||||
<div class="form-field"><label>Valor NF</label><input class="form-control form-control-sm" type="number" [(ngModel)]="controleEditModel.valorDaNf" (ngModelChange)="onControleEditValueChange()" /></div>
|
|
||||||
<div class="form-field"><label>Data NF</label><input class="form-control form-control-sm" type="date" [(ngModel)]="controleEditDataNf" (ngModelChange)="onControleEditDateChange()" /></div>
|
|
||||||
<div class="form-field"><label>Recebimento</label><input class="form-control form-control-sm" type="date" [(ngModel)]="controleEditRecebimento" /></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</details>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="modal-footer p-3 text-end border-top bg-white">
|
|
||||||
<button class="btn btn-glass btn-sm me-2" (click)="closeControleEdit()">Cancelar</button>
|
|
||||||
<button class="btn btn-primary btn-sm" [disabled]="controleEditSaving" (click)="saveControleEdit()">
|
|
||||||
{{ controleEditSaving ? 'Salvando...' : 'Salvar' }}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</ng-container>
|
|
||||||
|
|
||||||
<!-- MODAL CONTROLE DELETE -->
|
|
||||||
<ng-container *ngIf="controleDeleteOpen">
|
|
||||||
<div class="modal-card modal-lg" (click)="$event.stopPropagation()">
|
|
||||||
<div class="modal-header">
|
|
||||||
<div class="modal-title">
|
|
||||||
<span class="icon-bg danger-soft"><i class="bi bi-trash"></i></span>
|
|
||||||
Remover Recebimento
|
|
||||||
</div>
|
|
||||||
<button class="btn btn-sm btn-icon" (click)="cancelControleDelete()"><i class="bi bi-x-lg"></i></button>
|
|
||||||
</div>
|
|
||||||
<div class="modal-body modern-body bg-light-gray">
|
|
||||||
<div class="confirm-delete">
|
|
||||||
<div class="confirm-icon"><i class="bi bi-trash"></i></div>
|
|
||||||
<p class="mb-0">Confirma remover a NF <strong>{{ controleDeleteTarget?.notaFiscal }}</strong>?</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="modal-footer p-3 text-end border-top bg-white">
|
|
||||||
<button class="btn btn-glass btn-sm me-2" (click)="cancelControleDelete()">Cancelar</button>
|
|
||||||
<button class="btn btn-danger btn-sm" (click)="confirmControleDelete()">Excluir</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</ng-container>
|
|
||||||
</app-modal-layer>
|
|
||||||
|
|
@ -1,18 +0,0 @@
|
||||||
import { Component, Input } from '@angular/core';
|
|
||||||
import { CommonModule } from '@angular/common';
|
|
||||||
import { FormsModule } from '@angular/forms';
|
|
||||||
import { ModalLayerComponent } from '../../modal-layer/modal-layer';
|
|
||||||
import { VmProxyHost } from '../vm-proxy-host';
|
|
||||||
|
|
||||||
@Component({
|
|
||||||
selector: 'app-chips-controle-modals',
|
|
||||||
standalone: true,
|
|
||||||
imports: [CommonModule, FormsModule, ModalLayerComponent],
|
|
||||||
templateUrl: './chips-controle-modals.html',
|
|
||||||
styleUrls: ['../../../pages/chips-controle-recebidos/chips-controle-recebidos.scss'],
|
|
||||||
})
|
|
||||||
export class ChipsControleModalsComponent extends VmProxyHost {
|
|
||||||
@Input() set vm(value: any) {
|
|
||||||
this.attachVm(value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,277 +0,0 @@
|
||||||
<app-modal-layer
|
|
||||||
[open]="detailsOpen || editOpen || deleteOpen || createOpen"
|
|
||||||
(close)="closeDetails(); closeEdit(); cancelDelete(); closeCreate()"
|
|
||||||
>
|
|
||||||
<div *ngIf="detailsOpen" class="modal-card modal-lg" (click)="$event.stopPropagation()">
|
|
||||||
<div class="modal-header">
|
|
||||||
<div class="modal-title">
|
|
||||||
<span class="icon-bg primary-soft"><i class="bi bi-person-vcard"></i></span>
|
|
||||||
Detalhes do Usuário
|
|
||||||
</div>
|
|
||||||
<button class="btn-icon" (click)="closeDetails()"><i class="bi bi-x-lg"></i></button>
|
|
||||||
</div>
|
|
||||||
<div class="modal-body modern-body bg-light-gray">
|
|
||||||
<div class="details-dashboard">
|
|
||||||
<div class="detail-box w-100">
|
|
||||||
<div class="box-header"><span><i class="bi bi-card-text me-2"></i> Informações</span></div>
|
|
||||||
<div class="box-body">
|
|
||||||
<div class="form-grid">
|
|
||||||
<div class="form-field span-2"><label>CLIENTE</label><div class="fw-bold">{{ selectedRow?.cliente }}</div></div>
|
|
||||||
<div class="form-field"><label>TIPO</label><div>{{ (selectedRow?.tipoPessoa || 'PF') === 'PJ' ? 'PESSOA JURÍDICA' : 'PESSOA FÍSICA' }}</div></div>
|
|
||||||
<div class="form-field span-2"><label>{{ (selectedRow?.tipoPessoa || 'PF') === 'PJ' ? 'RAZÃO SOCIAL' : 'NOME' }}</label><div>{{ (selectedRow?.tipoPessoa || 'PF') === 'PJ' ? (selectedRow?.razaoSocial || selectedRow?.cliente || '-') : (selectedRow?.nome || selectedRow?.cliente || '-') }}</div></div>
|
|
||||||
<div class="form-field"><label>LINHA</label><div class="fw-black text-blue fs-5">{{ selectedRow?.linha }}</div></div>
|
|
||||||
<div class="form-field"><label>ITEM</label><div>{{ selectedRow?.item }}</div></div>
|
|
||||||
<div class="form-field"><label>{{ (selectedRow?.tipoPessoa || 'PF') === 'PJ' ? 'CNPJ' : 'CPF' }}</label><div>{{ (selectedRow?.tipoPessoa || 'PF') === 'PJ' ? (selectedRow?.cnpj || '-') : (selectedRow?.cpf || '-') }}</div></div>
|
|
||||||
<div class="form-field"><label>RG</label><div>{{ selectedRow?.rg || '-' }}</div></div>
|
|
||||||
|
|
||||||
<div class="form-field span-2"><label>E-MAIL</label><div>{{ selectedRow?.email || '-' }}</div></div>
|
|
||||||
|
|
||||||
<div class="form-field"><label>CELULAR</label><div>{{ selectedRow?.celular || '-' }}</div></div>
|
|
||||||
<div class="form-field"><label>TELEFONE FIXO</label><div>{{ selectedRow?.telefoneFixo || '-' }}</div></div>
|
|
||||||
|
|
||||||
<div class="form-field span-2"><label>ENDEREÇO</label><div>{{ selectedRow?.endereco || '-' }}</div></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- CREATE MODAL -->
|
|
||||||
<div *ngIf="createOpen" class="modal-card modal-xl-custom create-modal" (click)="$event.stopPropagation()">
|
|
||||||
<div class="modal-header">
|
|
||||||
<div class="modal-title">
|
|
||||||
<span class="icon-bg primary-soft"><i class="bi bi-plus-circle"></i></span>
|
|
||||||
Novo Usuário
|
|
||||||
</div>
|
|
||||||
<button class="btn-icon" (click)="closeCreate()"><i class="bi bi-x-lg"></i></button>
|
|
||||||
</div>
|
|
||||||
<div class="modal-body modern-body bg-light-gray" *ngIf="createModel">
|
|
||||||
<div class="edit-sections">
|
|
||||||
<details open class="detail-box">
|
|
||||||
<summary class="box-header">
|
|
||||||
<span><i class="bi bi-link-45deg me-2"></i> Vínculo com Reserva</span>
|
|
||||||
<i class="bi bi-chevron-down ms-auto transition-icon"></i>
|
|
||||||
</summary>
|
|
||||||
<div class="box-body">
|
|
||||||
<div class="form-grid">
|
|
||||||
<div class="form-field span-2">
|
|
||||||
<label>Linha (RESERVA)</label>
|
|
||||||
<app-select
|
|
||||||
class="form-select"
|
|
||||||
size="sm"
|
|
||||||
[options]="lineOptionsCreate"
|
|
||||||
labelKey="label"
|
|
||||||
valueKey="id"
|
|
||||||
[searchable]="true"
|
|
||||||
searchPlaceholder="Pesquisar linha da reserva..."
|
|
||||||
[(ngModel)]="createModel.mobileLineId"
|
|
||||||
(ngModelChange)="onCreateLineChange()"
|
|
||||||
[disabled]="createLinesLoading"
|
|
||||||
placeholder="Selecione uma linha da Reserva..."
|
|
||||||
></app-select>
|
|
||||||
<small class="field-hint" *ngIf="createLinesLoading">Carregando linhas da Reserva...</small>
|
|
||||||
</div>
|
|
||||||
<div class="form-field">
|
|
||||||
<label>Total Franquia Line</label>
|
|
||||||
<input class="form-control form-control-sm bg-light" [value]="formatFranquiaLine(createFranquiaLineTotal)" readonly />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</details>
|
|
||||||
|
|
||||||
<details open class="detail-box">
|
|
||||||
<summary class="box-header">
|
|
||||||
<span><i class="bi bi-person-vcard me-2"></i> Dados do Usuário</span>
|
|
||||||
<i class="bi bi-chevron-down ms-auto transition-icon"></i>
|
|
||||||
</summary>
|
|
||||||
<div class="box-body">
|
|
||||||
<div class="form-grid user-modal-grid">
|
|
||||||
<div class="form-field span-2"><label>Cliente</label><input class="form-control form-control-sm" [(ngModel)]="createModel.cliente" /></div>
|
|
||||||
<div class="form-field field-tipo"><label>Tipo</label>
|
|
||||||
<app-select
|
|
||||||
class="form-select"
|
|
||||||
size="sm"
|
|
||||||
[options]="tipoPessoaOptions"
|
|
||||||
labelKey="label"
|
|
||||||
valueKey="value"
|
|
||||||
[(ngModel)]="createModel.tipoPessoa"
|
|
||||||
(ngModelChange)="onCreateTipoChange()">
|
|
||||||
</app-select>
|
|
||||||
</div>
|
|
||||||
<div class="form-field span-2" *ngIf="(createModel.tipoPessoa || 'PF') === 'PF'">
|
|
||||||
<label>Nome</label>
|
|
||||||
<input class="form-control form-control-sm" [(ngModel)]="createModel.nome" />
|
|
||||||
</div>
|
|
||||||
<div class="form-field span-2" *ngIf="(createModel.tipoPessoa || 'PF') === 'PJ'">
|
|
||||||
<label>Razão Social</label>
|
|
||||||
<input class="form-control form-control-sm" [(ngModel)]="createModel.razaoSocial" />
|
|
||||||
</div>
|
|
||||||
<div class="form-field field-line">
|
|
||||||
<label>Linha</label>
|
|
||||||
<input class="form-control form-control-sm bg-light" [value]="createModel.linha || ''" readonly />
|
|
||||||
</div>
|
|
||||||
<div class="form-field field-item field-auto">
|
|
||||||
<label>Item (Automático)</label>
|
|
||||||
<input class="form-control form-control-sm bg-light" type="number" [(ngModel)]="createModel.item" readonly title="Gerado automaticamente pelo sistema" />
|
|
||||||
<small class="field-hint">Gerado automaticamente pelo sistema</small>
|
|
||||||
</div>
|
|
||||||
<div class="form-field field-cpf-cnpj" *ngIf="(createModel.tipoPessoa || 'PF') === 'PF'"><label>CPF</label><input class="form-control form-control-sm" [(ngModel)]="createModel.cpf" /></div>
|
|
||||||
<div class="form-field field-cpf-cnpj" *ngIf="(createModel.tipoPessoa || 'PF') === 'PJ'"><label>CNPJ</label><input class="form-control form-control-sm" [(ngModel)]="createModel.cnpj" /></div>
|
|
||||||
<div class="form-field field-rg"><label>RG</label><input class="form-control form-control-sm" [(ngModel)]="createModel.rg" /></div>
|
|
||||||
<div class="form-field span-2"><label>E-mail</label><input class="form-control form-control-sm" [(ngModel)]="createModel.email" /></div>
|
|
||||||
<div class="form-field span-2"><label>Endereço</label><input class="form-control form-control-sm" [(ngModel)]="createModel.endereco" /></div>
|
|
||||||
<div class="form-field field-celular"><label>Celular</label><input class="form-control form-control-sm" [(ngModel)]="createModel.celular" /></div>
|
|
||||||
<div class="form-field field-telefone"><label>Telefone Fixo</label><input class="form-control form-control-sm" [(ngModel)]="createModel.telefoneFixo" /></div>
|
|
||||||
<div class="form-field span-2" *ngIf="(createModel.tipoPessoa || 'PF') === 'PF'"><label>Data de Nascimento</label><input class="form-control form-control-sm" type="date" [(ngModel)]="createDateNascimento" /></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</details>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="modal-footer p-3 text-end border-top bg-white">
|
|
||||||
<button class="btn btn-glass btn-sm me-2" (click)="closeCreate()">Cancelar</button>
|
|
||||||
<button class="btn btn-brand btn-sm" [disabled]="createSaving" (click)="saveCreate()">
|
|
||||||
{{ createSaving ? 'Salvando...' : 'Salvar' }}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- EDIT MODAL -->
|
|
||||||
<div *ngIf="editOpen" class="modal-card modal-xl-custom" (click)="$event.stopPropagation()">
|
|
||||||
<div class="modal-header">
|
|
||||||
<div class="modal-title">
|
|
||||||
<span class="icon-bg primary-soft"><i class="bi bi-pencil-square"></i></span>
|
|
||||||
Editar Usuário
|
|
||||||
</div>
|
|
||||||
<button class="btn-icon" (click)="closeEdit()"><i class="bi bi-x-lg"></i></button>
|
|
||||||
</div>
|
|
||||||
<div class="modal-body modern-body bg-light-gray" *ngIf="editModel">
|
|
||||||
<div class="edit-sections">
|
|
||||||
<details open class="detail-box">
|
|
||||||
<summary class="box-header">
|
|
||||||
<span><i class="bi bi-person-badge me-2"></i> Identificação</span>
|
|
||||||
<i class="bi bi-chevron-down ms-auto transition-icon"></i>
|
|
||||||
</summary>
|
|
||||||
<div class="box-body">
|
|
||||||
<div class="form-grid user-modal-grid">
|
|
||||||
<div class="form-field span-2"><label>Cliente</label><input class="form-control form-control-sm" [(ngModel)]="editModel.cliente" /></div>
|
|
||||||
<div class="form-field field-tipo"><label>Tipo</label>
|
|
||||||
<app-select
|
|
||||||
class="form-select"
|
|
||||||
size="sm"
|
|
||||||
[options]="tipoPessoaOptions"
|
|
||||||
labelKey="label"
|
|
||||||
valueKey="value"
|
|
||||||
[(ngModel)]="editModel.tipoPessoa"
|
|
||||||
(ngModelChange)="onEditTipoChange()">
|
|
||||||
</app-select>
|
|
||||||
</div>
|
|
||||||
<div class="form-field span-2" *ngIf="(editModel.tipoPessoa || 'PF') === 'PF'">
|
|
||||||
<label>Nome</label>
|
|
||||||
<input class="form-control form-control-sm" [(ngModel)]="editModel.nome" />
|
|
||||||
</div>
|
|
||||||
<div class="form-field span-2" *ngIf="(editModel.tipoPessoa || 'PF') === 'PJ'">
|
|
||||||
<label>Razão Social</label>
|
|
||||||
<input class="form-control form-control-sm" [(ngModel)]="editModel.razaoSocial" />
|
|
||||||
</div>
|
|
||||||
<div class="form-field field-line">
|
|
||||||
<label>Linha (Reserva)</label>
|
|
||||||
<app-select
|
|
||||||
class="form-select"
|
|
||||||
size="sm"
|
|
||||||
[options]="editLineOptions"
|
|
||||||
labelKey="label"
|
|
||||||
valueKey="id"
|
|
||||||
[searchable]="true"
|
|
||||||
searchPlaceholder="Pesquisar linha da reserva..."
|
|
||||||
[(ngModel)]="editSelectedLineId"
|
|
||||||
(ngModelChange)="onEditLineChange()"
|
|
||||||
[disabled]="createLinesLoading"
|
|
||||||
placeholder="Selecione uma linha da Reserva..."
|
|
||||||
></app-select>
|
|
||||||
</div>
|
|
||||||
<div class="form-field field-auto">
|
|
||||||
<label>Total Franquia Line</label>
|
|
||||||
<input class="form-control form-control-sm bg-light" [value]="formatFranquiaLine(editFranquiaLineTotal)" readonly />
|
|
||||||
</div>
|
|
||||||
<div class="form-field field-item field-auto">
|
|
||||||
<label>Item (Automático)</label>
|
|
||||||
<input class="form-control form-control-sm bg-light" type="number" [(ngModel)]="editModel.item" readonly title="Gerado automaticamente pelo sistema" />
|
|
||||||
<small class="field-hint">Gerado automaticamente pelo sistema</small>
|
|
||||||
</div>
|
|
||||||
<div class="form-field field-cpf-cnpj" *ngIf="(editModel.tipoPessoa || 'PF') === 'PF'">
|
|
||||||
<label>CPF</label>
|
|
||||||
<input class="form-control form-control-sm" [(ngModel)]="editModel.cpf" />
|
|
||||||
</div>
|
|
||||||
<div class="form-field field-cpf-cnpj" *ngIf="(editModel.tipoPessoa || 'PF') === 'PJ'">
|
|
||||||
<label>CNPJ</label>
|
|
||||||
<input class="form-control form-control-sm" [(ngModel)]="editModel.cnpj" />
|
|
||||||
</div>
|
|
||||||
<div class="form-field field-rg"><label>RG</label><input class="form-control form-control-sm" [(ngModel)]="editModel.rg" /></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</details>
|
|
||||||
|
|
||||||
<details open class="detail-box">
|
|
||||||
<summary class="box-header">
|
|
||||||
<span><i class="bi bi-envelope-paper me-2"></i> Contato</span>
|
|
||||||
<i class="bi bi-chevron-down ms-auto transition-icon"></i>
|
|
||||||
</summary>
|
|
||||||
<div class="box-body">
|
|
||||||
<div class="form-grid user-modal-grid contact-modal-grid">
|
|
||||||
<div class="form-field span-2"><label>E-mail</label><input class="form-control form-control-sm" type="email" [(ngModel)]="editModel.email" /></div>
|
|
||||||
<div class="form-field field-celular"><label>Celular</label><input class="form-control form-control-sm" [(ngModel)]="editModel.celular" /></div>
|
|
||||||
<div class="form-field field-telefone"><label>Telefone Fixo</label><input class="form-control form-control-sm" [(ngModel)]="editModel.telefoneFixo" /></div>
|
|
||||||
<div class="form-field span-2"><label>Endereço</label><input class="form-control form-control-sm" [(ngModel)]="editModel.endereco" /></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</details>
|
|
||||||
|
|
||||||
<details open class="detail-box">
|
|
||||||
<summary class="box-header">
|
|
||||||
<span><i class="bi bi-calendar-event me-2"></i> Complemento</span>
|
|
||||||
<i class="bi bi-chevron-down ms-auto transition-icon"></i>
|
|
||||||
</summary>
|
|
||||||
<div class="box-body">
|
|
||||||
<div class="form-grid">
|
|
||||||
<div class="form-field" *ngIf="(editModel.tipoPessoa || 'PF') === 'PF'">
|
|
||||||
<label>Data Nascimento</label>
|
|
||||||
<input class="form-control form-control-sm" type="date" [(ngModel)]="editDateNascimento" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</details>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="modal-footer p-3 text-end border-top bg-white">
|
|
||||||
<button class="btn btn-glass btn-sm me-2" (click)="closeEdit()">Cancelar</button>
|
|
||||||
<button class="btn btn-primary btn-sm" [disabled]="editSaving" (click)="saveEdit()">
|
|
||||||
{{ editSaving ? 'Salvando...' : 'Salvar' }}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- DELETE MODAL -->
|
|
||||||
<div *ngIf="deleteOpen" class="modal-card modal-lg" (click)="$event.stopPropagation()">
|
|
||||||
<div class="modal-header">
|
|
||||||
<div class="modal-title">
|
|
||||||
<span class="icon-bg danger-soft"><i class="bi bi-trash"></i></span>
|
|
||||||
Remover Usuário
|
|
||||||
</div>
|
|
||||||
<button class="btn-icon" (click)="cancelDelete()"><i class="bi bi-x-lg"></i></button>
|
|
||||||
</div>
|
|
||||||
<div class="modal-body modern-body bg-light-gray">
|
|
||||||
<div class="confirm-delete">
|
|
||||||
<div class="confirm-icon"><i class="bi bi-trash"></i></div>
|
|
||||||
<p class="mb-0">Confirma remover o registro <strong>{{ deleteTarget?.linha }}</strong>?</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="modal-footer p-3 text-end border-top bg-white">
|
|
||||||
<button class="btn btn-glass btn-sm me-2" (click)="cancelDelete()">Cancelar</button>
|
|
||||||
<button class="btn btn-danger btn-sm" (click)="confirmDelete()">Excluir</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</app-modal-layer>
|
|
||||||
|
|
||||||
|
|
@ -1,19 +0,0 @@
|
||||||
import { Component, Input } from '@angular/core';
|
|
||||||
import { CommonModule } from '@angular/common';
|
|
||||||
import { FormsModule } from '@angular/forms';
|
|
||||||
import { CustomSelectComponent } from '../../custom-select/custom-select';
|
|
||||||
import { ModalLayerComponent } from '../../modal-layer/modal-layer';
|
|
||||||
import { VmProxyHost } from '../vm-proxy-host';
|
|
||||||
|
|
||||||
@Component({
|
|
||||||
selector: 'app-dados-usuarios-modals',
|
|
||||||
standalone: true,
|
|
||||||
imports: [CommonModule, FormsModule, CustomSelectComponent, ModalLayerComponent],
|
|
||||||
templateUrl: './dados-usuarios-modals.html',
|
|
||||||
styleUrls: ['../../../pages/dados-usuarios/dados-usuarios.scss'],
|
|
||||||
})
|
|
||||||
export class DadosUsuariosModalsComponent extends VmProxyHost {
|
|
||||||
@Input() set vm(value: any) {
|
|
||||||
this.attachVm(value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,246 +0,0 @@
|
||||||
<app-modal-layer
|
|
||||||
[open]="detailOpen || compareOpen || editOpen || deleteOpen"
|
|
||||||
(close)="closeAllModals()"
|
|
||||||
>
|
|
||||||
|
|
||||||
<!-- DETAIL MODAL -->
|
|
||||||
<div *ngIf="detailOpen" #detailModal class="modal-card modal-xl-custom" (click)="$event.stopPropagation()">
|
|
||||||
<div class="modal-header">
|
|
||||||
<div class="modal-title">
|
|
||||||
<span class="icon-bg detail-icon"><i class="bi bi-receipt"></i></span>
|
|
||||||
Detalhes do Faturamento
|
|
||||||
</div>
|
|
||||||
<button class="btn btn-sm btn-icon" (click)="closeAllModals()"><i class="bi bi-x-lg"></i></button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="modal-body modern-body bg-light-gray" *ngIf="detailData; else detailLoading">
|
|
||||||
<div class="mb-3 d-flex flex-wrap align-items-center justify-content-between gap-2">
|
|
||||||
<div class="d-flex flex-column">
|
|
||||||
<div class="fw-black detail-client">
|
|
||||||
{{ detailData.cliente || '—' }}
|
|
||||||
</div>
|
|
||||||
<small class="text-muted fw-bold">
|
|
||||||
ITEM: {{ detailData.item }} • QTD LINHAS: {{ detailData.qtdLinhas ?? 0 }}
|
|
||||||
</small>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="details-dashboard details-single">
|
|
||||||
|
|
||||||
<!-- IDENTIFICAÇÃO -->
|
|
||||||
<div class="detail-box">
|
|
||||||
<div class="box-header justify-content-center">
|
|
||||||
<span><i class="bi bi-person-badge me-2"></i> Identificação</span>
|
|
||||||
</div>
|
|
||||||
<div class="box-body">
|
|
||||||
<div class="info-grid">
|
|
||||||
<div class="info-item span-2">
|
|
||||||
<span class="lbl">Cliente</span>
|
|
||||||
<span class="val text-dark" [title]="detailData.cliente || ''">{{ detailData.cliente || '—' }}</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="info-item">
|
|
||||||
<span class="lbl">Tipo</span>
|
|
||||||
<span class="val">{{ detailData.tipo || '—' }}</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="info-item">
|
|
||||||
<span class="lbl">Qtd Linhas</span>
|
|
||||||
<span class="val">{{ detailData.qtdLinhas ?? 0 }}</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="info-item span-2">
|
|
||||||
<span class="lbl">Aparelho</span>
|
|
||||||
<span class="val" [title]="detailData.aparelho || ''">{{ detailData.aparelho || '—' }}</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="info-item span-2">
|
|
||||||
<span class="lbl">Forma de Pagamento</span>
|
|
||||||
<span class="val" [title]="detailData.formaPagamento || ''">{{ detailData.formaPagamento || '—' }}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<ng-template #detailLoading>
|
|
||||||
<div class="p-5 text-center text-muted">Carregando detalhes...</div>
|
|
||||||
</ng-template>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- COMPARATIVO MODAL -->
|
|
||||||
<div *ngIf="compareOpen" #compareModal class="modal-card modal-lg" (click)="$event.stopPropagation()">
|
|
||||||
<div class="modal-header">
|
|
||||||
<div class="modal-title">
|
|
||||||
<span class="icon-bg compare-icon"><i class="bi bi-columns-gap"></i></span> Comparativo Vivo x Line
|
|
||||||
</div>
|
|
||||||
<button class="btn btn-sm btn-icon" (click)="closeAllModals()"><i class="bi bi-x-lg"></i></button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="modal-body modern-body bg-light-gray" *ngIf="compareData; else compareLoading">
|
|
||||||
<div class="finance-dashboard">
|
|
||||||
<div class="finance-card vivo-card">
|
|
||||||
<div class="card-header-f"><i class="bi bi-telephone-fill me-2"></i> Vivo</div>
|
|
||||||
<div class="card-body-f">
|
|
||||||
<div class="row-item"><span>Franquia</span> <strong>{{ formatFranquia(compareData.franquiaVivo) }}</strong></div>
|
|
||||||
<div class="divider"></div>
|
|
||||||
<div class="row-item total"><span>Valor Vivo (R$)</span> <strong>{{ formatMoney(compareData.valorContratoVivo) }}</strong></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="finance-card line-card">
|
|
||||||
<div class="card-header-f"><i class="bi bi-hdd-network-fill me-2"></i> Line Móvel</div>
|
|
||||||
<div class="card-body-f">
|
|
||||||
<div class="row-item"><span>Franquia Line</span> <strong>{{ formatFranquia(compareData.franquiaLine) }}</strong></div>
|
|
||||||
<div class="divider"></div>
|
|
||||||
<div class="row-item total"><span>Valor Line (R$)</span> <strong>{{ formatMoney(compareData.valorContratoLine) }}</strong></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="finance-summary mt-3">
|
|
||||||
<div class="summary-item">
|
|
||||||
<span class="lbl">Forma de Pagamento</span>
|
|
||||||
<span class="val text-dark">{{ compareData.formaPagamento || '—' }}</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="vertical-line"></div>
|
|
||||||
|
|
||||||
<div class="summary-item">
|
|
||||||
<span class="lbl">Lucro</span>
|
|
||||||
<span class="val text-brand">{{ formatMoney(compareData.lucro) }}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<ng-template #compareLoading>
|
|
||||||
<div class="p-5 text-center text-muted">Carregando comparativo...</div>
|
|
||||||
</ng-template>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- EDIT MODAL -->
|
|
||||||
<div *ngIf="editOpen" class="modal-card modal-xl-custom" (click)="$event.stopPropagation()">
|
|
||||||
<div class="modal-header">
|
|
||||||
<div class="modal-title">
|
|
||||||
<span class="icon-bg primary-soft"><i class="bi bi-pencil-square"></i></span> Editar Faturamento
|
|
||||||
</div>
|
|
||||||
<button class="btn btn-sm btn-icon" (click)="closeEdit()"><i class="bi bi-x-lg"></i></button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="modal-body modern-body bg-light-gray" *ngIf="editModel">
|
|
||||||
<div class="edit-sections">
|
|
||||||
<details open class="detail-box">
|
|
||||||
<summary class="box-header">
|
|
||||||
<span><i class="bi bi-person-badge me-2"></i> Identificação</span>
|
|
||||||
<i class="bi bi-chevron-down ms-auto transition-icon"></i>
|
|
||||||
</summary>
|
|
||||||
<div class="box-body">
|
|
||||||
<div class="form-grid">
|
|
||||||
<div class="form-field span-2">
|
|
||||||
<label>Cliente</label>
|
|
||||||
<input class="form-control form-control-sm" [(ngModel)]="editModel.cliente" />
|
|
||||||
</div>
|
|
||||||
<div class="form-field">
|
|
||||||
<label>Tipo</label>
|
|
||||||
<select class="form-control form-control-sm" [(ngModel)]="editModel.tipo">
|
|
||||||
<option value="PF">PF</option>
|
|
||||||
<option value="PJ">PJ</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<div class="form-field">
|
|
||||||
<label>Item</label>
|
|
||||||
<input class="form-control form-control-sm" type="number" [(ngModel)]="editModel.item" />
|
|
||||||
</div>
|
|
||||||
<div class="form-field">
|
|
||||||
<label>Qtd Linhas</label>
|
|
||||||
<input class="form-control form-control-sm" type="number" [(ngModel)]="editModel.qtdLinhas" />
|
|
||||||
</div>
|
|
||||||
<div class="form-field">
|
|
||||||
<label>Aparelho</label>
|
|
||||||
<input class="form-control form-control-sm" [(ngModel)]="editModel.aparelho" />
|
|
||||||
</div>
|
|
||||||
<div class="form-field span-2">
|
|
||||||
<label>Forma de Pagamento</label>
|
|
||||||
<input class="form-control form-control-sm" [(ngModel)]="editModel.formaPagamento" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</details>
|
|
||||||
|
|
||||||
<details open class="detail-box vivo-border">
|
|
||||||
<summary class="box-header header-vivo">
|
|
||||||
<span><i class="bi bi-telephone-fill me-2"></i> Faturamento Vivo</span>
|
|
||||||
<i class="bi bi-chevron-down ms-auto transition-icon"></i>
|
|
||||||
</summary>
|
|
||||||
<div class="box-body">
|
|
||||||
<div class="form-grid">
|
|
||||||
<div class="form-field">
|
|
||||||
<label>Franquia Vivo</label>
|
|
||||||
<input class="form-control form-control-sm" type="number" [(ngModel)]="editModel.franquiaVivo" />
|
|
||||||
</div>
|
|
||||||
<div class="form-field">
|
|
||||||
<label>Valor Vivo (R$)</label>
|
|
||||||
<input class="form-control form-control-sm" type="number" [(ngModel)]="editModel.valorContratoVivo" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</details>
|
|
||||||
|
|
||||||
<details open class="detail-box line-border">
|
|
||||||
<summary class="box-header header-line">
|
|
||||||
<span><i class="bi bi-hdd-network-fill me-2"></i> Faturamento Line</span>
|
|
||||||
<i class="bi bi-chevron-down ms-auto transition-icon"></i>
|
|
||||||
</summary>
|
|
||||||
<div class="box-body">
|
|
||||||
<div class="form-grid">
|
|
||||||
<div class="form-field">
|
|
||||||
<label>Franquia Line</label>
|
|
||||||
<input class="form-control form-control-sm" type="number" [(ngModel)]="editModel.franquiaLine" />
|
|
||||||
</div>
|
|
||||||
<div class="form-field">
|
|
||||||
<label>Valor Line (R$)</label>
|
|
||||||
<input class="form-control form-control-sm" type="number" [(ngModel)]="editModel.valorContratoLine" />
|
|
||||||
</div>
|
|
||||||
<div class="form-field span-2">
|
|
||||||
<label>Lucro (R$)</label>
|
|
||||||
<input class="form-control form-control-sm" type="number" [(ngModel)]="editModel.lucro" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</details>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="modal-footer p-3 text-end border-top bg-white">
|
|
||||||
<button class="btn btn-glass btn-sm me-2" (click)="closeEdit()">Cancelar</button>
|
|
||||||
<button class="btn btn-primary btn-sm" [disabled]="editSaving" (click)="saveEdit()">
|
|
||||||
{{ editSaving ? 'Salvando...' : 'Salvar' }}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- DELETE MODAL -->
|
|
||||||
<div *ngIf="deleteOpen" class="modal-card modal-lg" (click)="$event.stopPropagation()">
|
|
||||||
<div class="modal-header">
|
|
||||||
<div class="modal-title">
|
|
||||||
<span class="icon-bg danger-soft"><i class="bi bi-trash"></i></span> Remover Faturamento
|
|
||||||
</div>
|
|
||||||
<button class="btn btn-sm btn-icon" (click)="cancelDelete()"><i class="bi bi-x-lg"></i></button>
|
|
||||||
</div>
|
|
||||||
<div class="modal-body modern-body bg-light-gray">
|
|
||||||
<div class="confirm-delete">
|
|
||||||
<div class="confirm-icon"><i class="bi bi-trash"></i></div>
|
|
||||||
<p class="mb-0">Confirma a exclusão do registro <strong>{{ deleteTarget?.cliente }}</strong>?</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="modal-footer p-3 text-end border-top bg-white">
|
|
||||||
<button class="btn btn-glass btn-sm me-2" (click)="cancelDelete()">Cancelar</button>
|
|
||||||
<button class="btn btn-danger btn-sm" (click)="confirmDelete()">Excluir</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</app-modal-layer>
|
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -1,18 +0,0 @@
|
||||||
import { Component, Input } from '@angular/core';
|
|
||||||
import { CommonModule } from '@angular/common';
|
|
||||||
import { FormsModule } from '@angular/forms';
|
|
||||||
import { ModalLayerComponent } from '../../modal-layer/modal-layer';
|
|
||||||
import { VmProxyHost } from '../vm-proxy-host';
|
|
||||||
|
|
||||||
@Component({
|
|
||||||
selector: 'app-faturamento-modals',
|
|
||||||
standalone: true,
|
|
||||||
imports: [CommonModule, FormsModule, ModalLayerComponent],
|
|
||||||
templateUrl: './faturamento-modals.html',
|
|
||||||
styleUrls: ['../../../pages/faturamento/faturamento.scss'],
|
|
||||||
})
|
|
||||||
export class FaturamentoModalsComponent extends VmProxyHost {
|
|
||||||
@Input() set vm(value: any) {
|
|
||||||
this.attachVm(value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -1,19 +0,0 @@
|
||||||
import { Component, Input } from '@angular/core';
|
|
||||||
import { CommonModule } from '@angular/common';
|
|
||||||
import { FormsModule } from '@angular/forms';
|
|
||||||
import { CustomSelectComponent } from '../../custom-select/custom-select';
|
|
||||||
import { ModalLayerComponent } from '../../modal-layer/modal-layer';
|
|
||||||
import { VmProxyHost } from '../vm-proxy-host';
|
|
||||||
|
|
||||||
@Component({
|
|
||||||
selector: 'app-geral-modals',
|
|
||||||
standalone: true,
|
|
||||||
imports: [CommonModule, FormsModule, CustomSelectComponent, ModalLayerComponent],
|
|
||||||
templateUrl: './geral-modals.html',
|
|
||||||
styleUrls: ['../../../pages/geral/geral.scss'],
|
|
||||||
})
|
|
||||||
export class GeralModalsComponent extends VmProxyHost {
|
|
||||||
@Input() set vm(value: any) {
|
|
||||||
this.attachVm(value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,309 +0,0 @@
|
||||||
<app-modal-layer
|
|
||||||
[open]="editOpen || createOpen || deleteOpen || detailOpen"
|
|
||||||
(close)="closeEdit(); closeCreate(); closeDelete(); closeDetail()"
|
|
||||||
>
|
|
||||||
|
|
||||||
<!-- ============================== -->
|
|
||||||
<!-- EDIT MODAL -->
|
|
||||||
<!-- ============================== -->
|
|
||||||
<div *ngIf="editOpen" class="modal-card modal-lg" (click)="$event.stopPropagation()">
|
|
||||||
|
|
||||||
<div class="modal-header">
|
|
||||||
<div class="modal-title">
|
|
||||||
<span class="icon-bg primary-soft"><i class="bi bi-pencil-square"></i></span>
|
|
||||||
Editar Registro Mureg
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="d-flex align-items-center gap-2">
|
|
||||||
<button class="btn btn-glass btn-sm" (click)="closeEdit()" [disabled]="editSaving">
|
|
||||||
<i class="bi bi-x-lg me-1"></i> Cancelar
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<button class="btn btn-brand btn-sm" (click)="saveEdit()" [disabled]="!editModel || editSaving">
|
|
||||||
<span *ngIf="!editSaving"><i class="bi bi-check2-circle me-1"></i> Salvar</span>
|
|
||||||
<span *ngIf="editSaving"><span class="spinner-border spinner-border-sm me-2"></span> Salvando...</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="modal-body modern-body bg-light-gray">
|
|
||||||
<ng-container *ngIf="editModel; else editLoadingTpl">
|
|
||||||
<div class="details-dashboard">
|
|
||||||
<div class="detail-box w-100">
|
|
||||||
|
|
||||||
<div class="box-header">
|
|
||||||
<span><i class="bi bi-card-text me-2"></i> Informações</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="box-body">
|
|
||||||
<div class="form-grid">
|
|
||||||
|
|
||||||
<!-- Cliente (select) -->
|
|
||||||
<div class="form-field span-2">
|
|
||||||
<label>Cliente (GERAL)</label>
|
|
||||||
<app-select class="form-control" size="sm" [options]="clientOptions" [(ngModel)]="editModel.selectedClient" (ngModelChange)="onEditClientChange()" placeholder="Selecione..."></app-select>
|
|
||||||
|
|
||||||
<small class="text-muted fw-bold" *ngIf="editClientsLoading">
|
|
||||||
<span class="spinner-border spinner-border-sm me-2"></span>Carregando clientes...
|
|
||||||
</small>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Linha Antiga (select da Geral) -->
|
|
||||||
<div class="form-field span-2">
|
|
||||||
<label>Linha Antiga (GERAL)</label>
|
|
||||||
<app-select class="form-control" size="sm" [options]="lineOptionsEdit" labelKey="label" valueKey="id" [(ngModel)]="editModel.mobileLineId" (ngModelChange)="onEditLineChange()" [disabled]="!editModel.selectedClient || editLinesLoading" placeholder="Selecione a linha do cliente..."></app-select>
|
|
||||||
|
|
||||||
<small class="text-muted fw-bold" *ngIf="editLinesLoading">
|
|
||||||
<span class="spinner-border spinner-border-sm me-2"></span>Carregando linhas...
|
|
||||||
</small>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="form-field">
|
|
||||||
<label>Item</label>
|
|
||||||
<input class="form-control form-control-sm" [(ngModel)]="editModel.item" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="form-field">
|
|
||||||
<label>Data Mureg</label>
|
|
||||||
<input class="form-control form-control-sm" type="date" [(ngModel)]="editModel.dataDaMureg" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- LinhaAntiga (snapshot) - preenchido automaticamente -->
|
|
||||||
<div class="form-field">
|
|
||||||
<label>Linha Antiga (snapshot)</label>
|
|
||||||
<input class="form-control form-control-sm" [(ngModel)]="editModel.linhaAntiga" readonly />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="form-field">
|
|
||||||
<label>Linha Nova</label>
|
|
||||||
<input class="form-control form-control-sm fw-bold text-blue" [(ngModel)]="editModel.linhaNova" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- ICCID auto do GERAL -->
|
|
||||||
<div class="form-field span-2">
|
|
||||||
<label>ICCID (auto)</label>
|
|
||||||
<input class="form-control form-control-sm font-monospace" [(ngModel)]="editModel.iccid" readonly />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="mt-3" *ngIf="editModel?.clienteInfo">
|
|
||||||
<small class="text-muted fw-bold">
|
|
||||||
<i class="bi bi-info-circle me-1"></i>
|
|
||||||
{{ editModel.clienteInfo }}
|
|
||||||
</small>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</ng-container>
|
|
||||||
|
|
||||||
<ng-template #editLoadingTpl>
|
|
||||||
<div class="p-5 text-center text-muted">
|
|
||||||
<span class="spinner-border me-2"></span> Preparando edição...
|
|
||||||
</div>
|
|
||||||
</ng-template>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- ============================== -->
|
|
||||||
<!-- CREATE MODAL -->
|
|
||||||
<!-- ============================== -->
|
|
||||||
<div *ngIf="createOpen" class="modal-card modal-lg" (click)="$event.stopPropagation()">
|
|
||||||
|
|
||||||
<div class="modal-header">
|
|
||||||
<div class="modal-title">
|
|
||||||
<span class="icon-bg brand-soft"><i class="bi bi-plus-lg"></i></span>
|
|
||||||
Nova Mureg
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="d-flex align-items-center gap-2">
|
|
||||||
<button class="btn btn-glass btn-sm" (click)="closeCreate()" [disabled]="createSaving">
|
|
||||||
<i class="bi bi-x-lg me-1"></i> Cancelar
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<button class="btn btn-brand btn-sm" (click)="saveCreate()" [disabled]="createSaving">
|
|
||||||
<span *ngIf="!createSaving"><i class="bi bi-check2-circle me-1"></i> Criar</span>
|
|
||||||
<span *ngIf="createSaving"><span class="spinner-border spinner-border-sm me-2"></span> Salvando...</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="modal-body modern-body bg-light-gray">
|
|
||||||
<div class="details-dashboard">
|
|
||||||
<div class="detail-box w-100">
|
|
||||||
|
|
||||||
<div class="box-header">
|
|
||||||
<span><i class="bi bi-pencil me-2"></i> Preencha os dados</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="box-body">
|
|
||||||
<div class="form-grid">
|
|
||||||
|
|
||||||
<!-- Cliente (select) -->
|
|
||||||
<div class="form-field span-2">
|
|
||||||
<label>Cliente (GERAL) <span class="text-danger">*</span></label>
|
|
||||||
<app-select class="form-control" size="sm" [options]="clientOptions" [(ngModel)]="createModel.selectedClient" (ngModelChange)="onCreateClientChange()" placeholder="Selecione..."></app-select>
|
|
||||||
|
|
||||||
<small class="text-muted fw-bold" *ngIf="createClientsLoading">
|
|
||||||
<span class="spinner-border spinner-border-sm me-2"></span>Carregando clientes...
|
|
||||||
</small>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Linha Antiga (select Geral) -->
|
|
||||||
<div class="form-field span-2">
|
|
||||||
<label>Linha Antiga (GERAL) <span class="text-danger">*</span></label>
|
|
||||||
<app-select class="form-control" size="sm" [options]="lineOptionsCreate" labelKey="label" valueKey="id" [(ngModel)]="createModel.mobileLineId" (ngModelChange)="onCreateLineChange()" [disabled]="!createModel.selectedClient || createLinesLoading" placeholder="Selecione a linha do cliente..."></app-select>
|
|
||||||
|
|
||||||
<small class="text-muted fw-bold" *ngIf="createLinesLoading">
|
|
||||||
<span class="spinner-border spinner-border-sm me-2"></span>Carregando linhas...
|
|
||||||
</small>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="form-field">
|
|
||||||
<label>Item</label>
|
|
||||||
<input class="form-control form-control-sm" [(ngModel)]="createModel.item" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="form-field">
|
|
||||||
<label>Data Mureg (automática)</label>
|
|
||||||
<input class="form-control form-control-sm" type="date" [(ngModel)]="createModel.dataDaMureg" readonly />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- snapshot -->
|
|
||||||
<div class="form-field">
|
|
||||||
<label>Linha Antiga (snapshot)</label>
|
|
||||||
<input class="form-control form-control-sm" [(ngModel)]="createModel.linhaAntiga" readonly />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="form-field">
|
|
||||||
<label>Linha Nova <span class="text-danger">*</span></label>
|
|
||||||
<input class="form-control form-control-sm fw-bold text-blue" [(ngModel)]="createModel.linhaNova" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- ICCID auto do GERAL -->
|
|
||||||
<div class="form-field span-2">
|
|
||||||
<label>ICCID (auto)</label>
|
|
||||||
<input class="form-control form-control-sm font-monospace" [(ngModel)]="createModel.iccid" readonly />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="mt-3" *ngIf="createModel?.clienteInfo">
|
|
||||||
<small class="text-muted fw-bold">
|
|
||||||
<i class="bi bi-info-circle me-1"></i>
|
|
||||||
{{ createModel.clienteInfo }}
|
|
||||||
</small>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- ============================== -->
|
|
||||||
<!-- DETAIL MODAL -->
|
|
||||||
<!-- ============================== -->
|
|
||||||
<div *ngIf="detailOpen" class="modal-card modal-xl-custom" (click)="$event.stopPropagation()">
|
|
||||||
|
|
||||||
<div class="modal-header">
|
|
||||||
<div class="modal-title">
|
|
||||||
<span class="icon-bg primary-soft"><i class="bi bi-eye"></i></span>
|
|
||||||
Detalhes da Mureg
|
|
||||||
</div>
|
|
||||||
<button class="btn btn-sm btn-icon" (click)="closeDetail()"><i class="bi bi-x-lg"></i></button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="modal-body modern-body bg-light-gray">
|
|
||||||
<div class="p-5 text-center text-muted" *ngIf="detailLoading">
|
|
||||||
<span class="spinner-border me-2"></span> Carregando detalhes...
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="details-dashboard" *ngIf="!detailLoading && detailData">
|
|
||||||
<div class="detail-box">
|
|
||||||
<div class="box-header justify-content-center">
|
|
||||||
<span><i class="bi bi-card-text me-2"></i> Informações da Mureg</span>
|
|
||||||
</div>
|
|
||||||
<div class="box-body">
|
|
||||||
<div class="info-grid">
|
|
||||||
<div class="info-item span-2">
|
|
||||||
<span class="lbl">Linha Nova</span>
|
|
||||||
<span class="val text-blue fs-4">{{ detailData.linhaNova || '-' }}</span>
|
|
||||||
</div>
|
|
||||||
<div class="info-item span-2">
|
|
||||||
<span class="lbl">Linha Antiga</span>
|
|
||||||
<span class="val">{{ detailData.linhaAntiga || '-' }}</span>
|
|
||||||
</div>
|
|
||||||
<div class="info-item span-2">
|
|
||||||
<span class="lbl">Cliente</span>
|
|
||||||
<span class="val">{{ detailData.cliente || '-' }}</span>
|
|
||||||
</div>
|
|
||||||
<div class="info-item span-2">
|
|
||||||
<span class="lbl">Usuário</span>
|
|
||||||
<span class="val">{{ detailData.usuario || '-' }}</span>
|
|
||||||
</div>
|
|
||||||
<div class="info-item">
|
|
||||||
<span class="lbl">Item</span>
|
|
||||||
<span class="val">{{ detailData.item || '-' }}</span>
|
|
||||||
</div>
|
|
||||||
<div class="info-item">
|
|
||||||
<span class="lbl">Data Mureg</span>
|
|
||||||
<span class="val">{{ displayValue('dataDaMureg', detailData.dataDaMureg) }}</span>
|
|
||||||
</div>
|
|
||||||
<div class="info-item span-2">
|
|
||||||
<span class="lbl">ICCID</span>
|
|
||||||
<span class="val small-text">{{ detailData.iccid || '-' }}</span>
|
|
||||||
</div>
|
|
||||||
<div class="info-item span-2">
|
|
||||||
<span class="lbl">Skil</span>
|
|
||||||
<span class="val">{{ detailData.skil || '-' }}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- ============================== -->
|
|
||||||
<!-- DELETE MODAL -->
|
|
||||||
<!-- ============================== -->
|
|
||||||
<div *ngIf="deleteOpen" class="modal-card modal-sm" (click)="$event.stopPropagation()">
|
|
||||||
|
|
||||||
<div class="modal-header">
|
|
||||||
<div class="modal-title">
|
|
||||||
<span class="icon-bg danger-soft"><i class="bi bi-trash"></i></span>
|
|
||||||
Excluir Mureg
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<button class="btn btn-glass btn-sm" (click)="closeDelete()" [disabled]="deleteSaving">
|
|
||||||
<i class="bi bi-x-lg me-1"></i> Cancelar
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="modal-body">
|
|
||||||
<p class="mb-2 fw-bold">Tem certeza que deseja excluir esta Mureg?</p>
|
|
||||||
<div class="text-muted small">
|
|
||||||
<div><strong>Cliente:</strong> {{ deleteTarget?.cliente || '-' }}</div>
|
|
||||||
<div><strong>Linha nova:</strong> {{ deleteTarget?.linhaNova || '-' }}</div>
|
|
||||||
<div><strong>Linha antiga:</strong> {{ deleteTarget?.linhaAntiga || '-' }}</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="d-flex justify-content-end gap-2 mt-4">
|
|
||||||
<button class="btn btn-glass btn-sm" (click)="closeDelete()" [disabled]="deleteSaving">
|
|
||||||
Cancelar
|
|
||||||
</button>
|
|
||||||
<button class="btn btn-danger btn-sm" (click)="confirmDelete()" [disabled]="deleteSaving">
|
|
||||||
<span *ngIf="!deleteSaving"><i class="bi bi-trash me-1"></i> Excluir</span>
|
|
||||||
<span *ngIf="deleteSaving"><span class="spinner-border spinner-border-sm me-2"></span> Excluindo...</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</app-modal-layer>
|
|
||||||
|
|
||||||
|
|
@ -1,19 +0,0 @@
|
||||||
import { Component, Input } from '@angular/core';
|
|
||||||
import { CommonModule } from '@angular/common';
|
|
||||||
import { FormsModule } from '@angular/forms';
|
|
||||||
import { CustomSelectComponent } from '../../custom-select/custom-select';
|
|
||||||
import { ModalLayerComponent } from '../../modal-layer/modal-layer';
|
|
||||||
import { VmProxyHost } from '../vm-proxy-host';
|
|
||||||
|
|
||||||
@Component({
|
|
||||||
selector: 'app-mureg-modals',
|
|
||||||
standalone: true,
|
|
||||||
imports: [CommonModule, FormsModule, CustomSelectComponent, ModalLayerComponent],
|
|
||||||
templateUrl: './mureg-modals.html',
|
|
||||||
styleUrls: ['../../../pages/mureg/mureg.scss'],
|
|
||||||
})
|
|
||||||
export class MuregModalsComponent extends VmProxyHost {
|
|
||||||
@Input() set vm(value: any) {
|
|
||||||
this.attachVm(value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,180 +0,0 @@
|
||||||
<!-- Modal detalhes -->
|
|
||||||
<app-modal-layer
|
|
||||||
[open]="detailOpen"
|
|
||||||
backdropClass="lg-backdrop"
|
|
||||||
overlayClass="lg-modal"
|
|
||||||
(close)="closeDetails()"
|
|
||||||
>
|
|
||||||
<div *ngIf="detailOpen" class="lg-modal-card parcelamento-modal" (click)="$event.stopPropagation()">
|
|
||||||
<div class="modal-header">
|
|
||||||
<div class="modal-title">
|
|
||||||
<span class="icon-bg"><i class="bi bi-card-list"></i></span>
|
|
||||||
<span>Detalhes do Parcelamento</span>
|
|
||||||
</div>
|
|
||||||
<div class="modal-actions">
|
|
||||||
<button class="btn-icon" type="button" (click)="closeDetails()" aria-label="Fechar modal">
|
|
||||||
<i class="bi bi-x-lg"></i>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="modal-body">
|
|
||||||
<div class="detail-state" *ngIf="detailLoading && !selectedDetail">
|
|
||||||
<div class="spinner-border text-brand" role="status"></div>
|
|
||||||
<span>Carregando detalhes...</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="detail-state error" *ngIf="!detailLoading && detailError && !selectedDetail">
|
|
||||||
<i class="bi bi-exclamation-triangle"></i>
|
|
||||||
<span>{{ detailError }}</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<ng-container *ngIf="selectedDetail as detail">
|
|
||||||
<div class="detail-grid">
|
|
||||||
<div class="detail-card">
|
|
||||||
<small>Cliente</small>
|
|
||||||
<span class="detail-strong">{{ detail.cliente || '-' }}</span>
|
|
||||||
</div>
|
|
||||||
<div class="detail-card">
|
|
||||||
<small>Linha</small>
|
|
||||||
<span class="detail-strong text-blue">{{ detail.linha || '-' }}</span>
|
|
||||||
</div>
|
|
||||||
<div class="detail-card">
|
|
||||||
<small>AnoRef</small>
|
|
||||||
<span>{{ detail.anoRef ?? '-' }}</span>
|
|
||||||
</div>
|
|
||||||
<div class="detail-card">
|
|
||||||
<small>Item</small>
|
|
||||||
<span>{{ detail.item ?? '-' }}</span>
|
|
||||||
</div>
|
|
||||||
<div class="detail-card">
|
|
||||||
<small>Qt Parcelas</small>
|
|
||||||
<span>{{ displayQtParcelas(detail) }}</span>
|
|
||||||
</div>
|
|
||||||
<div class="detail-card">
|
|
||||||
<small>Parcela Atual</small>
|
|
||||||
<span class="detail-strong">{{ detail.parcelaAtual ?? '-' }}</span>
|
|
||||||
</div>
|
|
||||||
<div class="detail-card">
|
|
||||||
<small>Total Parcelas</small>
|
|
||||||
<span>{{ detail.totalParcelas ?? '-' }}</span>
|
|
||||||
</div>
|
|
||||||
<div class="detail-card">
|
|
||||||
<small>Status</small>
|
|
||||||
<span class="status-pill">{{ detailStatus }}</span>
|
|
||||||
</div>
|
|
||||||
<div class="detail-card">
|
|
||||||
<small>Valor Cheio</small>
|
|
||||||
<span>{{ formatMoney(detail.valorCheio) }}</span>
|
|
||||||
</div>
|
|
||||||
<div class="detail-card">
|
|
||||||
<small>Desconto</small>
|
|
||||||
<span class="text-danger">{{ formatMoney(detail.desconto) }}</span>
|
|
||||||
</div>
|
|
||||||
<div class="detail-card highlight">
|
|
||||||
<small>Valor com Desconto</small>
|
|
||||||
<span class="detail-strong money-strong">{{ formatMoney(detail.valorComDesconto) }}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="annual-section">
|
|
||||||
<div class="annual-head">
|
|
||||||
<div class="section-title">
|
|
||||||
<i class="bi bi-table"></i>
|
|
||||||
<span>Detalhamento anual</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="annual-table-shell" *ngIf="annualRows.length > 0; else annualEmpty">
|
|
||||||
<table class="table-modern annual-table">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th class="sticky-col col-1">Ano</th>
|
|
||||||
<th class="sticky-col col-2 text-end">Total</th>
|
|
||||||
<th *ngFor="let m of annualMonthHeaders" class="text-end">{{ m.label }}</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
<tr *ngFor="let row of annualRows">
|
|
||||||
<td class="sticky-col col-1">{{ row.year }}</td>
|
|
||||||
<td class="sticky-col col-2 text-end">{{ row.total | currency:'BRL':'symbol':'1.2-2':'pt-BR' }}</td>
|
|
||||||
<td *ngFor="let m of row.months" class="text-end">
|
|
||||||
{{ m.value !== null && m.value !== undefined ? (m.value | currency:'BRL':'symbol':'1.2-2':'pt-BR') : '-' }}
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<ng-template #annualEmpty>
|
|
||||||
<div class="annual-empty">
|
|
||||||
Sem dados anuais.
|
|
||||||
</div>
|
|
||||||
</ng-template>
|
|
||||||
</div>
|
|
||||||
</ng-container>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="modal-footer">
|
|
||||||
<button class="btn-primary" type="button" (click)="closeDetails()">Fechar</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</app-modal-layer>
|
|
||||||
|
|
||||||
<app-parcelamento-create-modal
|
|
||||||
[open]="createOpen"
|
|
||||||
[model]="createModel"
|
|
||||||
[monthOptions]="monthOptions"
|
|
||||||
[loading]="createSaving"
|
|
||||||
[errorMessage]="createError"
|
|
||||||
title="Novo Parcelamento"
|
|
||||||
submitLabel="Salvar"
|
|
||||||
(close)="closeCreateModal()"
|
|
||||||
(save)="saveNewParcelamento($event)">
|
|
||||||
</app-parcelamento-create-modal>
|
|
||||||
|
|
||||||
<app-parcelamento-create-modal
|
|
||||||
*ngIf="editOpen && editModel"
|
|
||||||
[open]="editOpen"
|
|
||||||
[model]="editModel"
|
|
||||||
[monthOptions]="monthOptions"
|
|
||||||
[loading]="editSaving"
|
|
||||||
[errorMessage]="editError"
|
|
||||||
title="Editar Parcelamento"
|
|
||||||
submitLabel="Atualizar"
|
|
||||||
(close)="closeEditModal()"
|
|
||||||
(save)="saveEditParcelamento($event)">
|
|
||||||
</app-parcelamento-create-modal>
|
|
||||||
|
|
||||||
<!-- Delete modal -->
|
|
||||||
<app-modal-layer
|
|
||||||
[open]="deleteOpen"
|
|
||||||
backdropClass="lg-backdrop"
|
|
||||||
overlayClass="lg-modal"
|
|
||||||
(close)="cancelDelete()"
|
|
||||||
>
|
|
||||||
<div *ngIf="deleteOpen" class="lg-modal-card modal-compact" (click)="$event.stopPropagation()">
|
|
||||||
<div class="modal-header">
|
|
||||||
<div class="modal-title">
|
|
||||||
<span class="icon-bg danger-soft"><i class="bi bi-trash"></i></span>
|
|
||||||
Remover Parcelamento
|
|
||||||
</div>
|
|
||||||
<button class="btn-icon" type="button" (click)="cancelDelete()" aria-label="Fechar modal de exclusao">
|
|
||||||
<i class="bi bi-x-lg"></i>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<div class="modal-body">
|
|
||||||
<div class="confirm-delete">
|
|
||||||
<div class="confirm-icon"><i class="bi bi-trash"></i></div>
|
|
||||||
<p class="mb-0">Confirma remover o parcelamento <strong>{{ deleteTarget?.linha }}</strong>?</p>
|
|
||||||
<small class="text-danger" *ngIf="deleteError">{{ deleteError }}</small>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="modal-footer">
|
|
||||||
<button class="btn-ghost" type="button" (click)="cancelDelete()">Cancelar</button>
|
|
||||||
<button class="btn-danger" type="button" [disabled]="deleteLoading" (click)="confirmDelete()">
|
|
||||||
{{ deleteLoading ? 'Excluindo...' : 'Excluir' }}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</app-modal-layer>
|
|
||||||
|
|
@ -1,18 +0,0 @@
|
||||||
import { Component, Input } from '@angular/core';
|
|
||||||
import { CommonModule } from '@angular/common';
|
|
||||||
import { ModalLayerComponent } from '../../modal-layer/modal-layer';
|
|
||||||
import { ParcelamentoCreateModalComponent } from './components/parcelamento-create-modal/parcelamento-create-modal';
|
|
||||||
import { VmProxyHost } from '../vm-proxy-host';
|
|
||||||
|
|
||||||
@Component({
|
|
||||||
selector: 'app-parcelamentos-modals',
|
|
||||||
standalone: true,
|
|
||||||
imports: [CommonModule, ModalLayerComponent, ParcelamentoCreateModalComponent],
|
|
||||||
templateUrl: './parcelamentos-modals.html',
|
|
||||||
styleUrls: ['../../../pages/parcelamentos/parcelamentos.scss'],
|
|
||||||
})
|
|
||||||
export class ParcelamentosModalsComponent extends VmProxyHost {
|
|
||||||
@Input() set vm(value: any) {
|
|
||||||
this.attachVm(value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,181 +0,0 @@
|
||||||
<app-modal-layer
|
|
||||||
[open]="editOpen || createOpen"
|
|
||||||
(close)="closeEdit(); closeCreate()"
|
|
||||||
>
|
|
||||||
|
|
||||||
<!-- EDIT MODAL -->
|
|
||||||
<div *ngIf="editOpen" class="modal-card modal-lg" (click)="$event.stopPropagation()">
|
|
||||||
<div class="modal-header">
|
|
||||||
<div class="modal-title">
|
|
||||||
<span class="icon-bg primary-soft"><i class="bi bi-pencil-square"></i></span>
|
|
||||||
Editar Troca de Número
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="d-flex align-items-center gap-2">
|
|
||||||
<button class="btn btn-glass btn-sm" (click)="closeEdit()" [disabled]="editSaving">
|
|
||||||
<i class="bi bi-x-lg me-1"></i> Cancelar
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<button class="btn btn-brand btn-sm" (click)="saveEdit()" [disabled]="!editModel || editSaving">
|
|
||||||
<span *ngIf="!editSaving"><i class="bi bi-check2-circle me-1"></i> Salvar</span>
|
|
||||||
<span *ngIf="editSaving"><span class="spinner-border spinner-border-sm me-2"></span> Salvando...</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="modal-body modern-body bg-light-gray">
|
|
||||||
<ng-container *ngIf="editModel; else editLoadingTpl">
|
|
||||||
<div class="details-dashboard">
|
|
||||||
<div class="detail-box w-100">
|
|
||||||
<div class="box-header"><span><i class="bi bi-card-text me-2"></i> Informações</span></div>
|
|
||||||
|
|
||||||
<div class="box-body">
|
|
||||||
<div class="form-grid">
|
|
||||||
<div class="form-field">
|
|
||||||
<label>Item</label>
|
|
||||||
<input class="form-control form-control-sm" [(ngModel)]="editModel.item" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="form-field">
|
|
||||||
<label>Data Troca</label>
|
|
||||||
<input class="form-control form-control-sm" type="date" [(ngModel)]="editModel.dataTroca" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="form-field">
|
|
||||||
<label>Linha Antiga</label>
|
|
||||||
<input class="form-control form-control-sm" [(ngModel)]="editModel.linhaAntiga" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="form-field">
|
|
||||||
<label>Linha Nova</label>
|
|
||||||
<input class="form-control form-control-sm fw-bold text-blue" [(ngModel)]="editModel.linhaNova" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="form-field span-2">
|
|
||||||
<label>ICCID</label>
|
|
||||||
<input class="form-control form-control-sm font-monospace" [(ngModel)]="editModel.iccid" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="form-field span-2">
|
|
||||||
<label>Motivo</label>
|
|
||||||
<input class="form-control form-control-sm" [(ngModel)]="editModel.motivo" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="form-field span-2">
|
|
||||||
<label>Observação</label>
|
|
||||||
<textarea class="form-control form-control-sm" rows="3" [(ngModel)]="editModel.observacao"></textarea>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</ng-container>
|
|
||||||
|
|
||||||
<ng-template #editLoadingTpl>
|
|
||||||
<div class="p-5 text-center text-muted">Preparando edição...</div>
|
|
||||||
</ng-template>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- CREATE MODAL (✅ BEBENDO DO GERAL) -->
|
|
||||||
<div *ngIf="createOpen" class="modal-card modal-lg" (click)="$event.stopPropagation()">
|
|
||||||
<div class="modal-header">
|
|
||||||
<div class="modal-title">
|
|
||||||
<span class="icon-bg brand-soft"><i class="bi bi-plus-lg"></i></span>
|
|
||||||
Nova Troca
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="d-flex align-items-center gap-2">
|
|
||||||
<button class="btn btn-glass btn-sm" (click)="closeCreate()" [disabled]="createSaving">
|
|
||||||
<i class="bi bi-x-lg me-1"></i> Cancelar
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<button class="btn btn-brand btn-sm" (click)="saveCreate()" [disabled]="createSaving">
|
|
||||||
<span *ngIf="!createSaving"><i class="bi bi-check2-circle me-1"></i> Criar</span>
|
|
||||||
<span *ngIf="createSaving"><span class="spinner-border spinner-border-sm me-2"></span> Salvando...</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="modal-body modern-body bg-light-gray">
|
|
||||||
<div class="details-dashboard">
|
|
||||||
<div class="detail-box w-100">
|
|
||||||
<div class="box-header"><span><i class="bi bi-pencil me-2"></i> Preencha os dados</span></div>
|
|
||||||
|
|
||||||
<div class="box-body">
|
|
||||||
<div class="form-grid">
|
|
||||||
|
|
||||||
<div class="form-field">
|
|
||||||
<label>Item</label>
|
|
||||||
<input class="form-control form-control-sm" [(ngModel)]="createModel.item" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="form-field">
|
|
||||||
<label>Data Troca</label>
|
|
||||||
<input class="form-control form-control-sm" type="date" [(ngModel)]="createModel.dataTroca" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- ✅ Cliente (GERAL) -->
|
|
||||||
<div class="form-field span-2">
|
|
||||||
<label>Cliente (GERAL)</label>
|
|
||||||
<app-select class="form-control" size="sm" [options]="clientsFromGeral" [(ngModel)]="selectedCliente" (ngModelChange)="onClienteChange()" placeholder="Selecione..."></app-select>
|
|
||||||
|
|
||||||
<small class="hint" *ngIf="loadingClients">
|
|
||||||
<span class="spinner-border spinner-border-sm me-2"></span> Carregando clientes...
|
|
||||||
</small>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- ✅ Linha do Cliente (GERAL) -->
|
|
||||||
<div class="form-field span-2">
|
|
||||||
<label>Linha do Cliente (GERAL)</label>
|
|
||||||
<app-select class="form-control" size="sm" [options]="linesFromClient" labelKey="label" valueKey="id" [(ngModel)]="selectedLineId" (ngModelChange)="onLineChange()" [disabled]="!selectedCliente || loadingLines" placeholder="Selecione a linha do cliente..."></app-select>
|
|
||||||
|
|
||||||
<small class="hint" *ngIf="loadingLines">
|
|
||||||
<span class="spinner-border spinner-border-sm me-2"></span> Carregando linhas...
|
|
||||||
</small>
|
|
||||||
|
|
||||||
<small class="hint warn" *ngIf="selectedCliente && !loadingLines && linesFromClient.length === 0">
|
|
||||||
Nenhuma linha encontrada para este cliente no GERAL.
|
|
||||||
</small>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- ✅ Linha Antiga (auto do GERAL) -->
|
|
||||||
<div class="form-field">
|
|
||||||
<label>Linha Antiga (auto)</label>
|
|
||||||
<input class="form-control form-control-sm" [(ngModel)]="createModel.linhaAntiga" readonly />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Linha Nova -->
|
|
||||||
<div class="form-field">
|
|
||||||
<label>Linha Nova</label>
|
|
||||||
<input class="form-control form-control-sm fw-bold text-blue" [(ngModel)]="createModel.linhaNova" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- ✅ ICCID (auto do GERAL) -->
|
|
||||||
<div class="form-field span-2">
|
|
||||||
<label>ICCID (auto)</label>
|
|
||||||
<input class="form-control form-control-sm font-monospace" [(ngModel)]="createModel.iccid" readonly />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="form-field span-2">
|
|
||||||
<label>Motivo</label>
|
|
||||||
<input class="form-control form-control-sm" [(ngModel)]="createModel.motivo" placeholder="Ex: perda/roubo, troca de colaborador..." />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="form-field span-2">
|
|
||||||
<label>Observação</label>
|
|
||||||
<textarea class="form-control form-control-sm" rows="3" [(ngModel)]="createModel.observacao"></textarea>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</app-modal-layer>
|
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -1,19 +0,0 @@
|
||||||
import { Component, Input } from '@angular/core';
|
|
||||||
import { CommonModule } from '@angular/common';
|
|
||||||
import { FormsModule } from '@angular/forms';
|
|
||||||
import { CustomSelectComponent } from '../../custom-select/custom-select';
|
|
||||||
import { ModalLayerComponent } from '../../modal-layer/modal-layer';
|
|
||||||
import { VmProxyHost } from '../vm-proxy-host';
|
|
||||||
|
|
||||||
@Component({
|
|
||||||
selector: 'app-troca-numero-modals',
|
|
||||||
standalone: true,
|
|
||||||
imports: [CommonModule, FormsModule, CustomSelectComponent, ModalLayerComponent],
|
|
||||||
templateUrl: './troca-numero-modals.html',
|
|
||||||
styleUrls: ['../../../pages/troca-numero/troca-numero.scss'],
|
|
||||||
})
|
|
||||||
export class TrocaNumeroModalsComponent extends VmProxyHost {
|
|
||||||
@Input() set vm(value: any) {
|
|
||||||
this.attachVm(value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,262 +0,0 @@
|
||||||
<app-modal-layer
|
|
||||||
[open]="detailsOpen || editOpen || deleteOpen || createOpen"
|
|
||||||
[showBackdrop]="false"
|
|
||||||
overlayClass="lg-modal"
|
|
||||||
(close)="closeDetails(); closeEdit(); cancelDelete(); closeCreate()"
|
|
||||||
>
|
|
||||||
<div *ngIf="detailsOpen" class="lg-modal-card modal-lg" (click)="$event.stopPropagation()">
|
|
||||||
<div class="modal-header">
|
|
||||||
<div class="modal-title">
|
|
||||||
<span class="icon-bg primary-soft"><i class="bi bi-card-list"></i></span>
|
|
||||||
Detalhes da Vigência
|
|
||||||
</div>
|
|
||||||
<button class="btn-icon" (click)="closeDetails()"><i class="bi bi-x-lg"></i></button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="modal-body bg-light-gray">
|
|
||||||
<div class="details-dashboard">
|
|
||||||
<div class="detail-box">
|
|
||||||
<div class="box-header justify-content-center">
|
|
||||||
<span><i class="bi bi-card-text me-2"></i> Informações da Linha</span>
|
|
||||||
</div>
|
|
||||||
<div class="box-body">
|
|
||||||
<div class="info-grid">
|
|
||||||
<div class="info-item span-2">
|
|
||||||
<span class="lbl">Cliente</span>
|
|
||||||
<span class="val">{{ selectedRow?.cliente || '-' }}</span>
|
|
||||||
</div>
|
|
||||||
<div class="info-item">
|
|
||||||
<span class="lbl">Linha</span>
|
|
||||||
<span class="val fw-black text-blue">{{ selectedRow?.linha || '-' }}</span>
|
|
||||||
</div>
|
|
||||||
<div class="info-item">
|
|
||||||
<span class="lbl">Conta</span>
|
|
||||||
<span class="val">{{ selectedRow?.conta || '-' }}</span>
|
|
||||||
</div>
|
|
||||||
<div class="info-item span-2">
|
|
||||||
<span class="lbl">Usuário</span>
|
|
||||||
<span class="val">{{ selectedRow?.usuario || '-' }}</span>
|
|
||||||
</div>
|
|
||||||
<div class="info-item span-2">
|
|
||||||
<span class="lbl">Plano</span>
|
|
||||||
<span class="val">{{ selectedRow?.planoContrato || '-' }}</span>
|
|
||||||
</div>
|
|
||||||
<div class="info-item">
|
|
||||||
<span class="lbl">Efetivação</span>
|
|
||||||
<span class="val">{{ selectedRow?.dtEfetivacaoServico ? (selectedRow?.dtEfetivacaoServico | date:'dd/MM/yyyy') : '-' }}</span>
|
|
||||||
</div>
|
|
||||||
<div class="info-item">
|
|
||||||
<span class="lbl">Término</span>
|
|
||||||
<span class="val" [class.text-danger]="isVencido(selectedRow?.dtTerminoFidelizacao)">
|
|
||||||
{{ selectedRow?.dtTerminoFidelizacao ? (selectedRow?.dtTerminoFidelizacao | date:'dd/MM/yyyy') : '-' }}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div class="info-item">
|
|
||||||
<span class="lbl">Situação</span>
|
|
||||||
<span class="status-pill" [class.is-danger]="isVencido(selectedRow?.dtTerminoFidelizacao)">
|
|
||||||
{{ isVencido(selectedRow?.dtTerminoFidelizacao) ? 'Vencido' : 'Ativo' }}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div class="info-item">
|
|
||||||
<span class="lbl">Renovação</span>
|
|
||||||
<span class="val">
|
|
||||||
{{ selectedRow?.autoRenewYears ? ('Auto +' + selectedRow?.autoRenewYears + ' ano(s)') : 'Não programada' }}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div class="info-item">
|
|
||||||
<span class="lbl">Valor Total</span>
|
|
||||||
<span class="val text-brand">{{ (selectedRow?.total || 0) | currency:'BRL' }}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="modal-footer p-3 text-end border-top">
|
|
||||||
<button class="btn btn-glass btn-sm" (click)="closeDetails()">Fechar</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- CREATE MODAL -->
|
|
||||||
<div *ngIf="createOpen" class="lg-modal-card modal-xl create-modal" (click)="$event.stopPropagation()">
|
|
||||||
<div class="modal-header">
|
|
||||||
<div class="modal-title">
|
|
||||||
<span class="icon-bg primary-soft"><i class="bi bi-plus-circle"></i></span>
|
|
||||||
Nova Vigência
|
|
||||||
</div>
|
|
||||||
<button class="btn-icon" (click)="closeCreate()"><i class="bi bi-x-lg"></i></button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="modal-body bg-light-gray">
|
|
||||||
<div class="edit-sections">
|
|
||||||
<details open class="detail-box">
|
|
||||||
<summary class="box-header">
|
|
||||||
<span><i class="bi bi-link-45deg me-2"></i> Vínculo com GERAL</span>
|
|
||||||
<i class="bi bi-chevron-down ms-auto transition-icon"></i>
|
|
||||||
</summary>
|
|
||||||
<div class="box-body">
|
|
||||||
<div class="form-grid">
|
|
||||||
<div class="form-field span-2">
|
|
||||||
<label>Cliente (GERAL)</label>
|
|
||||||
<app-select
|
|
||||||
class="form-select"
|
|
||||||
size="sm"
|
|
||||||
[options]="clientsFromGeral"
|
|
||||||
[(ngModel)]="createModel.selectedClient"
|
|
||||||
(ngModelChange)="onCreateClientChange()"
|
|
||||||
[disabled]="createClientsLoading"
|
|
||||||
></app-select>
|
|
||||||
</div>
|
|
||||||
<div class="form-field span-2">
|
|
||||||
<label>Linha (GERAL)</label>
|
|
||||||
<app-select
|
|
||||||
class="form-select"
|
|
||||||
size="sm"
|
|
||||||
[options]="lineOptionsCreate"
|
|
||||||
labelKey="label"
|
|
||||||
valueKey="id"
|
|
||||||
[(ngModel)]="createModel.mobileLineId"
|
|
||||||
(ngModelChange)="onCreateLineChange()"
|
|
||||||
[disabled]="createLinesLoading || !createModel.selectedClient"
|
|
||||||
></app-select>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</details>
|
|
||||||
|
|
||||||
<details open class="detail-box">
|
|
||||||
<summary class="box-header">
|
|
||||||
<span><i class="bi bi-person-badge me-2"></i> Identificação</span>
|
|
||||||
<i class="bi bi-chevron-down ms-auto transition-icon"></i>
|
|
||||||
</summary>
|
|
||||||
<div class="box-body">
|
|
||||||
<div class="form-grid">
|
|
||||||
<div class="form-field"><label>Cliente</label><input class="form-control form-control-sm" [(ngModel)]="createModel.cliente" /></div>
|
|
||||||
<div class="form-field field-line"><label>Linha</label><input class="form-control form-control-sm" inputmode="numeric" [(ngModel)]="createModel.linha" /></div>
|
|
||||||
<div class="form-field"><label>Conta</label><input class="form-control form-control-sm" [(ngModel)]="createModel.conta" /></div>
|
|
||||||
<div class="form-field"><label>Usuário</label><input class="form-control form-control-sm" [(ngModel)]="createModel.usuario" /></div>
|
|
||||||
<div class="form-field span-2">
|
|
||||||
<label>Plano</label>
|
|
||||||
<app-select
|
|
||||||
*ngIf="planOptions.length > 0"
|
|
||||||
class="form-select"
|
|
||||||
size="sm"
|
|
||||||
[options]="planOptions"
|
|
||||||
[(ngModel)]="createModel.planoContrato"
|
|
||||||
(ngModelChange)="onCreatePlanChange()"
|
|
||||||
></app-select>
|
|
||||||
<input
|
|
||||||
*ngIf="planOptions.length === 0"
|
|
||||||
class="form-control form-control-sm"
|
|
||||||
[(ngModel)]="createModel.planoContrato"
|
|
||||||
(ngModelChange)="onCreatePlanChange()"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div class="form-field field-item field-auto">
|
|
||||||
<label>Item (Automático)</label>
|
|
||||||
<input class="form-control form-control-sm bg-light" type="number" [(ngModel)]="createModel.item" readonly title="Gerado automaticamente pelo sistema" />
|
|
||||||
<small class="field-hint">Gerado automaticamente pelo sistema</small>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</details>
|
|
||||||
|
|
||||||
<details open class="detail-box">
|
|
||||||
<summary class="box-header">
|
|
||||||
<span><i class="bi bi-calendar-event me-2"></i> Vigência e Valor</span>
|
|
||||||
<i class="bi bi-chevron-down ms-auto transition-icon"></i>
|
|
||||||
</summary>
|
|
||||||
<div class="box-body">
|
|
||||||
<div class="form-grid">
|
|
||||||
<div class="form-field"><label>Efetivação</label><input class="form-control form-control-sm" type="date" [(ngModel)]="createEfetivacao" /></div>
|
|
||||||
<div class="form-field"><label>Término</label><input class="form-control form-control-sm" type="date" [(ngModel)]="createTermino" /></div>
|
|
||||||
<div class="form-field span-2"><label>Total</label><input class="form-control form-control-sm" type="number" [(ngModel)]="createModel.total" /></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</details>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="modal-footer p-3 text-end border-top">
|
|
||||||
<button class="btn btn-glass btn-sm me-2" (click)="closeCreate()">Cancelar</button>
|
|
||||||
<button class="btn btn-brand btn-sm" [disabled]="createSaving" (click)="saveCreate()">
|
|
||||||
{{ createSaving ? 'Salvando...' : 'Salvar' }}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- EDIT MODAL -->
|
|
||||||
<div *ngIf="editOpen" class="lg-modal-card modal-xl" (click)="$event.stopPropagation()">
|
|
||||||
<div class="modal-header">
|
|
||||||
<div class="modal-title">
|
|
||||||
<span class="icon-bg primary-soft"><i class="bi bi-pencil-square"></i></span>
|
|
||||||
Editar Vigência
|
|
||||||
</div>
|
|
||||||
<button class="btn-icon" (click)="closeEdit()"><i class="bi bi-x-lg"></i></button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="modal-body bg-light-gray" *ngIf="editModel">
|
|
||||||
<div class="edit-sections">
|
|
||||||
<details open class="detail-box">
|
|
||||||
<summary class="box-header">
|
|
||||||
<span><i class="bi bi-person-badge me-2"></i> Identificação</span>
|
|
||||||
<i class="bi bi-chevron-down ms-auto transition-icon"></i>
|
|
||||||
</summary>
|
|
||||||
<div class="box-body">
|
|
||||||
<div class="form-grid">
|
|
||||||
<div class="form-field"><label>Cliente</label><input class="form-control form-control-sm" [(ngModel)]="editModel.cliente" /></div>
|
|
||||||
<div class="form-field field-line"><label>Linha</label><input class="form-control form-control-sm" inputmode="numeric" [(ngModel)]="editModel.linha" /></div>
|
|
||||||
<div class="form-field"><label>Conta</label><input class="form-control form-control-sm" [(ngModel)]="editModel.conta" /></div>
|
|
||||||
<div class="form-field"><label>Usuário</label><input class="form-control form-control-sm" [(ngModel)]="editModel.usuario" /></div>
|
|
||||||
<div class="form-field span-2"><label>Plano</label><input class="form-control form-control-sm" [(ngModel)]="editModel.planoContrato" (ngModelChange)="onEditPlanChange()" /></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</details>
|
|
||||||
|
|
||||||
<details open class="detail-box">
|
|
||||||
<summary class="box-header">
|
|
||||||
<span><i class="bi bi-calendar-event me-2"></i> Vigência e Valor</span>
|
|
||||||
<i class="bi bi-chevron-down ms-auto transition-icon"></i>
|
|
||||||
</summary>
|
|
||||||
<div class="box-body">
|
|
||||||
<div class="form-grid">
|
|
||||||
<div class="form-field"><label>Efetivação</label><input class="form-control form-control-sm" type="date" [(ngModel)]="editEfetivacao" /></div>
|
|
||||||
<div class="form-field"><label>Término</label><input class="form-control form-control-sm" type="date" [(ngModel)]="editTermino" /></div>
|
|
||||||
<div class="form-field span-2"><label>Total</label><input class="form-control form-control-sm" type="number" [(ngModel)]="editModel.total" /></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</details>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="modal-footer p-3 text-end border-top">
|
|
||||||
<button class="btn btn-glass btn-sm me-2" (click)="closeEdit()">Cancelar</button>
|
|
||||||
<button class="btn btn-primary btn-sm" [disabled]="editSaving" (click)="saveEdit()">
|
|
||||||
{{ editSaving ? 'Salvando...' : 'Salvar' }}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- DELETE MODAL -->
|
|
||||||
<div *ngIf="deleteOpen" class="lg-modal-card modal-lg" (click)="$event.stopPropagation()">
|
|
||||||
<div class="modal-header">
|
|
||||||
<div class="modal-title">
|
|
||||||
<span class="icon-bg danger-soft"><i class="bi bi-trash"></i></span>
|
|
||||||
Remover Vigência
|
|
||||||
</div>
|
|
||||||
<button class="btn-icon" (click)="cancelDelete()"><i class="bi bi-x-lg"></i></button>
|
|
||||||
</div>
|
|
||||||
<div class="modal-body bg-light-gray">
|
|
||||||
<div class="confirm-delete">
|
|
||||||
<div class="confirm-icon"><i class="bi bi-trash"></i></div>
|
|
||||||
<p class="mb-0">Confirma remover o registro <strong>{{ deleteTarget?.linha }}</strong>?</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="modal-footer p-3 text-end border-top">
|
|
||||||
<button class="btn btn-glass btn-sm me-2" (click)="cancelDelete()">Cancelar</button>
|
|
||||||
<button class="btn btn-danger btn-sm" (click)="confirmDelete()">Excluir</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</app-modal-layer>
|
|
||||||
|
|
||||||
|
|
@ -1,19 +0,0 @@
|
||||||
import { Component, Input } from '@angular/core';
|
|
||||||
import { CommonModule } from '@angular/common';
|
|
||||||
import { FormsModule } from '@angular/forms';
|
|
||||||
import { CustomSelectComponent } from '../../custom-select/custom-select';
|
|
||||||
import { ModalLayerComponent } from '../../modal-layer/modal-layer';
|
|
||||||
import { VmProxyHost } from '../vm-proxy-host';
|
|
||||||
|
|
||||||
@Component({
|
|
||||||
selector: 'app-vigencia-modals',
|
|
||||||
standalone: true,
|
|
||||||
imports: [CommonModule, FormsModule, CustomSelectComponent, ModalLayerComponent],
|
|
||||||
templateUrl: './vigencia-modals.html',
|
|
||||||
styleUrls: ['../../../pages/vigencia/vigencia.scss'],
|
|
||||||
})
|
|
||||||
export class VigenciaModalsComponent extends VmProxyHost {
|
|
||||||
@Input() set vm(value: any) {
|
|
||||||
this.attachVm(value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,52 +0,0 @@
|
||||||
export abstract class VmProxyHost {
|
|
||||||
[key: string]: any;
|
|
||||||
|
|
||||||
private __boundKeys = new Set<string>();
|
|
||||||
|
|
||||||
protected attachVm(vm: any): void {
|
|
||||||
if (!vm) return;
|
|
||||||
|
|
||||||
// Bind own enumerable fields so template reads/writes keep source state in sync.
|
|
||||||
Object.keys(vm).forEach((key) => this.bindField(vm, key));
|
|
||||||
|
|
||||||
let proto: any = Object.getPrototypeOf(vm);
|
|
||||||
while (proto && proto !== Object.prototype) {
|
|
||||||
for (const key of Object.getOwnPropertyNames(proto)) {
|
|
||||||
if (key === 'constructor' || key.startsWith('ng')) continue;
|
|
||||||
|
|
||||||
const descriptor = Object.getOwnPropertyDescriptor(proto, key);
|
|
||||||
if (!descriptor) continue;
|
|
||||||
|
|
||||||
if (descriptor.get || descriptor.set) {
|
|
||||||
this.bindField(vm, key);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
const value = (vm as any)[key];
|
|
||||||
if (typeof value === 'function' && !this.__boundKeys.has(key)) {
|
|
||||||
(this as any)[key] = (...args: any[]) => value.apply(vm, args);
|
|
||||||
this.__boundKeys.add(key);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
proto = Object.getPrototypeOf(proto);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private bindField(vm: any, key: string): void {
|
|
||||||
if (this.__boundKeys.has(key)) return;
|
|
||||||
|
|
||||||
if (Object.prototype.hasOwnProperty.call(this, key)) return;
|
|
||||||
|
|
||||||
Object.defineProperty(this, key, {
|
|
||||||
configurable: true,
|
|
||||||
enumerable: true,
|
|
||||||
get: () => (vm as any)[key],
|
|
||||||
set: (value: unknown) => {
|
|
||||||
(vm as any)[key] = value;
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
this.__boundKeys.add(key);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,27 +0,0 @@
|
||||||
import { inject, PLATFORM_ID } from '@angular/core';
|
|
||||||
import { CanActivateFn, Router } from '@angular/router';
|
|
||||||
import { isPlatformBrowser } from '@angular/common';
|
|
||||||
import { AuthService } from '../services/auth.service';
|
|
||||||
|
|
||||||
export const sysadminOrFinanceiroGuard: CanActivateFn = () => {
|
|
||||||
const router = inject(Router);
|
|
||||||
const platformId = inject(PLATFORM_ID);
|
|
||||||
const authService = inject(AuthService);
|
|
||||||
|
|
||||||
if (!isPlatformBrowser(platformId)) {
|
|
||||||
// Em SSR não há storage do usuário para validar sessão/perfil.
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
const token = authService.token;
|
|
||||||
if (!token) {
|
|
||||||
return router.parseUrl('/login');
|
|
||||||
}
|
|
||||||
|
|
||||||
const hasAccess = authService.hasRole('sysadmin') || authService.hasRole('financeiro');
|
|
||||||
if (!hasAccess) {
|
|
||||||
return router.parseUrl('/dashboard');
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
};
|
|
||||||
|
|
@ -18,10 +18,7 @@ export const sysadminOrGestorGuard: CanActivateFn = () => {
|
||||||
return router.parseUrl('/login');
|
return router.parseUrl('/login');
|
||||||
}
|
}
|
||||||
|
|
||||||
const hasAccess =
|
const hasAccess = authService.hasRole('sysadmin') || authService.hasRole('gestor');
|
||||||
authService.hasRole('sysadmin') ||
|
|
||||||
authService.hasRole('gestor') ||
|
|
||||||
authService.hasRole('financeiro');
|
|
||||||
if (!hasAccess) {
|
if (!hasAccess) {
|
||||||
return router.parseUrl('/dashboard');
|
return router.parseUrl('/dashboard');
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -35,14 +35,6 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="header-actions d-flex gap-2 justify-content-end">
|
<div class="header-actions d-flex gap-2 justify-content-end">
|
||||||
<button
|
|
||||||
class="btn btn-glass btn-sm"
|
|
||||||
(click)="onExport()"
|
|
||||||
[disabled]="activeLoading || exporting"
|
|
||||||
>
|
|
||||||
<span *ngIf="!exporting"><i class="bi bi-download me-1"></i> Exportar</span>
|
|
||||||
<span *ngIf="exporting"><span class="spinner-border spinner-border-sm me-2"></span> Exportando...</span>
|
|
||||||
</button>
|
|
||||||
<button
|
<button
|
||||||
*ngIf="isSysAdmin && activeTab === 'chips'"
|
*ngIf="isSysAdmin && activeTab === 'chips'"
|
||||||
class="btn btn-brand btn-sm"
|
class="btn btn-brand btn-sm"
|
||||||
|
|
@ -392,4 +384,372 @@
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<app-chips-controle-modals [vm]="$any(vm)"></app-chips-controle-modals>
|
<div class="modal-backdrop-custom" *ngIf="chipDetailOpen || controleDetailOpen || chipEditOpen || chipDeleteOpen || controleEditOpen || controleDeleteOpen || chipCreateOpen || controleCreateOpen" (click)="closeChipDetail(); closeControleDetail(); closeChipEdit(); cancelChipDelete(); closeControleEdit(); cancelControleDelete(); closeChipCreate(); closeControleCreate()"></div>
|
||||||
|
|
||||||
|
<!-- MODAL CHIP -->
|
||||||
|
<div class="modal-custom" *ngIf="chipDetailOpen">
|
||||||
|
<div class="modal-card modal-lg" (click)="$event.stopPropagation()">
|
||||||
|
<div class="modal-header">
|
||||||
|
<div class="modal-title">
|
||||||
|
<span class="icon-bg primary-soft"><i class="bi bi-sim"></i></span>
|
||||||
|
Detalhes do Chip
|
||||||
|
</div>
|
||||||
|
<button class="btn btn-sm btn-icon" (click)="closeChipDetail()"><i class="bi bi-x-lg"></i></button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="modal-body modern-body bg-light-gray">
|
||||||
|
<div class="p-5 text-center text-muted" *ngIf="chipDetailLoading">
|
||||||
|
<span class="spinner-border me-2"></span> Carregando detalhes...
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="details-dashboard" *ngIf="!chipDetailLoading && chipDetailData">
|
||||||
|
<div class="detail-box w-100">
|
||||||
|
<div class="box-header justify-content-center">
|
||||||
|
<span><i class="bi bi-card-text me-2"></i> Informações do Chip</span>
|
||||||
|
</div>
|
||||||
|
<div class="box-body">
|
||||||
|
<div class="info-grid">
|
||||||
|
<div class="info-item">
|
||||||
|
<span class="lbl">Item</span>
|
||||||
|
<span class="val">{{ display(chipDetailData.item) }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="info-item span-2">
|
||||||
|
<span class="lbl">Número do Chip</span>
|
||||||
|
<span class="val text-brand font-monospace">{{ display(chipDetailData.numeroDoChip) }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="info-item span-2">
|
||||||
|
<span class="lbl">Observações</span>
|
||||||
|
<span class="val">{{ display(chipDetailData.observacoes) }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- MODAL CONTROLE -->
|
||||||
|
<div class="modal-custom" *ngIf="controleDetailOpen">
|
||||||
|
<div class="modal-card modal-xl-custom" (click)="$event.stopPropagation()">
|
||||||
|
<div class="modal-header">
|
||||||
|
<div class="modal-title">
|
||||||
|
<span class="icon-bg primary-soft"><i class="bi bi-clipboard-data"></i></span>
|
||||||
|
Detalhes do Recebimento
|
||||||
|
</div>
|
||||||
|
<button class="btn btn-sm btn-icon" (click)="closeControleDetail()"><i class="bi bi-x-lg"></i></button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="modal-body modern-body bg-light-gray">
|
||||||
|
<div class="p-5 text-center text-muted" *ngIf="controleDetailLoading">
|
||||||
|
<span class="spinner-border me-2"></span> Carregando detalhes...
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="details-dashboard" *ngIf="!controleDetailLoading && controleDetailData">
|
||||||
|
<div class="detail-box w-100">
|
||||||
|
<div class="box-header justify-content-center">
|
||||||
|
<span><i class="bi bi-card-text me-2"></i> Informações da NF</span>
|
||||||
|
</div>
|
||||||
|
<div class="box-body">
|
||||||
|
<div class="info-grid">
|
||||||
|
<div class="info-item">
|
||||||
|
<span class="lbl">Ano</span>
|
||||||
|
<span class="val">{{ display(controleDetailData.ano) }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="info-item">
|
||||||
|
<span class="lbl">Item</span>
|
||||||
|
<span class="val">{{ display(controleDetailData.item) }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="info-item span-2">
|
||||||
|
<span class="lbl">Nota Fiscal</span>
|
||||||
|
<span class="val">{{ display(controleDetailData.notaFiscal) }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="info-item span-2">
|
||||||
|
<span class="lbl">Chip</span>
|
||||||
|
<span class="val font-monospace">{{ display(controleDetailData.chip) }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="info-item span-2">
|
||||||
|
<span class="lbl">Serial</span>
|
||||||
|
<span class="val font-monospace">{{ display(controleDetailData.serial) }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="info-item span-2">
|
||||||
|
<span class="lbl">Conteúdo da NF</span>
|
||||||
|
<span class="val">{{ display(controleDetailData.conteudoDaNf) }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="info-item">
|
||||||
|
<span class="lbl">Número da Linha</span>
|
||||||
|
<span class="val font-monospace">{{ display(controleDetailData.numeroDaLinha) }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="info-item">
|
||||||
|
<span class="lbl">Quantidade</span>
|
||||||
|
<span class="val">{{ display(controleDetailData.quantidade) }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="info-item">
|
||||||
|
<span class="lbl">Valor Unit</span>
|
||||||
|
<span class="val">{{ formatMoney(controleDetailData.valorUnit) }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="info-item">
|
||||||
|
<span class="lbl">Valor da NF</span>
|
||||||
|
<span class="val text-brand">{{ formatMoney(controleDetailData.valorDaNf) }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="info-item">
|
||||||
|
<span class="lbl">Data da NF</span>
|
||||||
|
<span class="val">{{ formatDate(controleDetailData.dataDaNf) }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="info-item">
|
||||||
|
<span class="lbl">Recebimento</span>
|
||||||
|
<span class="val">{{ formatDate(controleDetailData.dataDoRecebimento) }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="info-item">
|
||||||
|
<span class="lbl">Tipo</span>
|
||||||
|
<span class="val">{{ isResumo(controleDetailData) ? "RESUMO" : "DETALHE" }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- MODAL CHIP CREATE -->
|
||||||
|
<div class="modal-custom" *ngIf="chipCreateOpen">
|
||||||
|
<div class="modal-card modal-lg create-modal" (click)="$event.stopPropagation()">
|
||||||
|
<div class="modal-header">
|
||||||
|
<div class="modal-title">
|
||||||
|
<span class="icon-bg primary-soft"><i class="bi bi-plus-circle"></i></span>
|
||||||
|
Novo Chip
|
||||||
|
</div>
|
||||||
|
<button class="btn btn-sm btn-icon" (click)="closeChipCreate()"><i class="bi bi-x-lg"></i></button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body modern-body bg-light-gray" *ngIf="chipCreateModel">
|
||||||
|
<div class="edit-sections">
|
||||||
|
<details open class="detail-box">
|
||||||
|
<summary class="box-header">
|
||||||
|
<span><i class="bi bi-sim me-2"></i> Informações do Chip</span>
|
||||||
|
<i class="bi bi-chevron-down ms-auto transition-icon"></i>
|
||||||
|
</summary>
|
||||||
|
<div class="box-body">
|
||||||
|
<div class="form-grid">
|
||||||
|
<div class="form-field"><label>Item (opcional)</label><input class="form-control form-control-sm" type="number" [(ngModel)]="chipCreateModel.item" /></div>
|
||||||
|
<div class="form-field span-2"><label>Número do Chip</label><input class="form-control form-control-sm" [(ngModel)]="chipCreateModel.numeroDoChip" /></div>
|
||||||
|
<div class="form-field span-2"><label>Observações</label><input class="form-control form-control-sm" [(ngModel)]="chipCreateModel.observacoes" /></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</details>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer p-3 text-end border-top bg-white">
|
||||||
|
<button class="btn btn-glass btn-sm me-2" (click)="closeChipCreate()">Cancelar</button>
|
||||||
|
<button class="btn btn-brand btn-sm" [disabled]="chipCreateSaving" (click)="saveChipCreate()">
|
||||||
|
{{ chipCreateSaving ? 'Salvando...' : 'Salvar' }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- MODAL CONTROLE CREATE -->
|
||||||
|
<div class="modal-custom" *ngIf="controleCreateOpen">
|
||||||
|
<div class="modal-card modal-xl-custom create-modal" (click)="$event.stopPropagation()">
|
||||||
|
<div class="modal-header">
|
||||||
|
<div class="modal-title">
|
||||||
|
<span class="icon-bg primary-soft"><i class="bi bi-plus-circle"></i></span>
|
||||||
|
Novo Recebimento
|
||||||
|
</div>
|
||||||
|
<button class="btn btn-sm btn-icon" (click)="closeControleCreate()"><i class="bi bi-x-lg"></i></button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body modern-body bg-light-gray" *ngIf="controleCreateModel">
|
||||||
|
<div class="edit-sections">
|
||||||
|
<details open class="detail-box">
|
||||||
|
<summary class="box-header">
|
||||||
|
<span><i class="bi bi-card-list me-2"></i> Dados da Nota</span>
|
||||||
|
<i class="bi bi-chevron-down ms-auto transition-icon"></i>
|
||||||
|
</summary>
|
||||||
|
<div class="box-body">
|
||||||
|
<div class="form-grid">
|
||||||
|
<div class="form-field"><label>Ano</label><input class="form-control form-control-sm" type="number" [(ngModel)]="controleCreateModel.ano" /></div>
|
||||||
|
<div class="form-field"><label>Item</label><input class="form-control form-control-sm" type="number" [(ngModel)]="controleCreateModel.item" /></div>
|
||||||
|
<div class="form-field span-2"><label>Nota Fiscal</label><input class="form-control form-control-sm" [(ngModel)]="controleCreateModel.notaFiscal" /></div>
|
||||||
|
<div class="form-field span-2"><label>Conteúdo da NF</label><input class="form-control form-control-sm" [(ngModel)]="controleCreateModel.conteudoDaNf" /></div>
|
||||||
|
<div class="form-field"><label>Serial</label><input class="form-control form-control-sm" [(ngModel)]="controleCreateModel.serial" /></div>
|
||||||
|
<div class="form-field"><label>Chip</label><input class="form-control form-control-sm" [(ngModel)]="controleCreateModel.chip" /></div>
|
||||||
|
<div class="form-field"><label>Número da Linha</label><input class="form-control form-control-sm" [(ngModel)]="controleCreateModel.numeroDaLinha" /></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</details>
|
||||||
|
|
||||||
|
<details open class="detail-box">
|
||||||
|
<summary class="box-header">
|
||||||
|
<span><i class="bi bi-currency-exchange me-2"></i> Valores e Datas</span>
|
||||||
|
<i class="bi bi-chevron-down ms-auto transition-icon"></i>
|
||||||
|
</summary>
|
||||||
|
<div class="box-body">
|
||||||
|
<div class="form-grid">
|
||||||
|
<div class="form-field"><label>Valor Unit</label><input class="form-control form-control-sm" type="number" [(ngModel)]="controleCreateModel.valorUnit" (ngModelChange)="onControleCreateValueChange()" /></div>
|
||||||
|
<div class="form-field"><label>Quantidade</label><input class="form-control form-control-sm" type="number" [(ngModel)]="controleCreateModel.quantidade" (ngModelChange)="onControleCreateValueChange()" /></div>
|
||||||
|
<div class="form-field"><label>Valor da NF</label><input class="form-control form-control-sm" type="number" [(ngModel)]="controleCreateModel.valorDaNf" (ngModelChange)="onControleCreateValueChange()" /></div>
|
||||||
|
<div class="form-field"><label>Data da NF</label><input class="form-control form-control-sm" type="date" [(ngModel)]="controleCreateDataNf" (ngModelChange)="onControleCreateDateChange()" /></div>
|
||||||
|
<div class="form-field"><label>Recebimento</label><input class="form-control form-control-sm" type="date" [(ngModel)]="controleCreateRecebimento" /></div>
|
||||||
|
<div class="form-field"><label>Resumo</label><input class="form-check-input ms-2" type="checkbox" [(ngModel)]="controleCreateModel.isResumo" /></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</details>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer p-3 text-end border-top bg-white">
|
||||||
|
<button class="btn btn-glass btn-sm me-2" (click)="closeControleCreate()">Cancelar</button>
|
||||||
|
<button class="btn btn-brand btn-sm" [disabled]="controleCreateSaving" (click)="saveControleCreate()">
|
||||||
|
{{ controleCreateSaving ? 'Salvando...' : 'Salvar' }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- MODAL CHIP EDIT -->
|
||||||
|
<div class="modal-custom" *ngIf="chipEditOpen">
|
||||||
|
<div class="modal-card modal-lg" (click)="$event.stopPropagation()">
|
||||||
|
<div class="modal-header">
|
||||||
|
<div class="modal-title">
|
||||||
|
<span class="icon-bg primary-soft"><i class="bi bi-pencil-square"></i></span>
|
||||||
|
Editar Chip
|
||||||
|
</div>
|
||||||
|
<button class="btn btn-sm btn-icon" (click)="closeChipEdit()"><i class="bi bi-x-lg"></i></button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body modern-body bg-light-gray" *ngIf="chipEditModel">
|
||||||
|
<div class="edit-sections">
|
||||||
|
<details open class="detail-box">
|
||||||
|
<summary class="box-header">
|
||||||
|
<span><i class="bi bi-sim me-2"></i> Identificação do Chip</span>
|
||||||
|
<i class="bi bi-chevron-down ms-auto transition-icon"></i>
|
||||||
|
</summary>
|
||||||
|
<div class="box-body">
|
||||||
|
<div class="form-grid">
|
||||||
|
<div class="form-field"><label>Item</label><input class="form-control form-control-sm" type="number" [(ngModel)]="chipEditModel.item" /></div>
|
||||||
|
<div class="form-field span-2"><label>Número do Chip</label><input class="form-control form-control-sm" [(ngModel)]="chipEditModel.numeroDoChip" /></div>
|
||||||
|
<div class="form-field span-2"><label>Observações</label><input class="form-control form-control-sm" [(ngModel)]="chipEditModel.observacoes" /></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</details>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer p-3 text-end border-top bg-white">
|
||||||
|
<button class="btn btn-glass btn-sm me-2" (click)="closeChipEdit()">Cancelar</button>
|
||||||
|
<button class="btn btn-primary btn-sm" [disabled]="chipEditSaving" (click)="saveChipEdit()">
|
||||||
|
{{ chipEditSaving ? 'Salvando...' : 'Salvar' }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- MODAL CHIP DELETE -->
|
||||||
|
<div class="modal-custom" *ngIf="chipDeleteOpen">
|
||||||
|
<div class="modal-card modal-lg" (click)="$event.stopPropagation()">
|
||||||
|
<div class="modal-header">
|
||||||
|
<div class="modal-title">
|
||||||
|
<span class="icon-bg danger-soft"><i class="bi bi-trash"></i></span>
|
||||||
|
Remover Chip
|
||||||
|
</div>
|
||||||
|
<button class="btn btn-sm btn-icon" (click)="cancelChipDelete()"><i class="bi bi-x-lg"></i></button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body modern-body bg-light-gray">
|
||||||
|
<div class="confirm-delete">
|
||||||
|
<div class="confirm-icon"><i class="bi bi-trash"></i></div>
|
||||||
|
<p class="mb-0">Confirma remover o chip <strong>{{ chipDeleteTarget?.numeroDoChip }}</strong>?</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer p-3 text-end border-top bg-white">
|
||||||
|
<button class="btn btn-glass btn-sm me-2" (click)="cancelChipDelete()">Cancelar</button>
|
||||||
|
<button class="btn btn-danger btn-sm" (click)="confirmChipDelete()">Excluir</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- MODAL CONTROLE EDIT -->
|
||||||
|
<div class="modal-custom" *ngIf="controleEditOpen">
|
||||||
|
<div class="modal-card modal-xl-custom" (click)="$event.stopPropagation()">
|
||||||
|
<div class="modal-header">
|
||||||
|
<div class="modal-title">
|
||||||
|
<span class="icon-bg primary-soft"><i class="bi bi-pencil-square"></i></span>
|
||||||
|
Editar Recebimento
|
||||||
|
</div>
|
||||||
|
<button class="btn btn-sm btn-icon" (click)="closeControleEdit()"><i class="bi bi-x-lg"></i></button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body modern-body bg-light-gray" *ngIf="controleEditModel">
|
||||||
|
<div class="edit-sections">
|
||||||
|
<details open class="detail-box">
|
||||||
|
<summary class="box-header">
|
||||||
|
<span><i class="bi bi-receipt-cutoff me-2"></i> Documento</span>
|
||||||
|
<i class="bi bi-chevron-down ms-auto transition-icon"></i>
|
||||||
|
</summary>
|
||||||
|
<div class="box-body">
|
||||||
|
<div class="form-grid">
|
||||||
|
<div class="form-field"><label>Ano</label><input class="form-control form-control-sm" type="number" [(ngModel)]="controleEditModel.ano" /></div>
|
||||||
|
<div class="form-field"><label>Item</label><input class="form-control form-control-sm" type="number" [(ngModel)]="controleEditModel.item" /></div>
|
||||||
|
<div class="form-field span-2"><label>Nota Fiscal</label><input class="form-control form-control-sm" [(ngModel)]="controleEditModel.notaFiscal" /></div>
|
||||||
|
<div class="form-field"><label>Chip</label><input class="form-control form-control-sm" [(ngModel)]="controleEditModel.chip" /></div>
|
||||||
|
<div class="form-field"><label>Serial</label><input class="form-control form-control-sm" [(ngModel)]="controleEditModel.serial" /></div>
|
||||||
|
<div class="form-field span-2"><label>Conteúdo da NF</label><input class="form-control form-control-sm" [(ngModel)]="controleEditModel.conteudoDaNf" /></div>
|
||||||
|
<div class="form-field"><label>Número da Linha</label><input class="form-control form-control-sm" [(ngModel)]="controleEditModel.numeroDaLinha" /></div>
|
||||||
|
<div class="form-field"><label>Tipo</label>
|
||||||
|
<select class="form-control form-control-sm" [(ngModel)]="controleEditModel.isResumo">
|
||||||
|
<option [ngValue]="false">DETALHE</option>
|
||||||
|
<option [ngValue]="true">RESUMO</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</details>
|
||||||
|
|
||||||
|
<details open class="detail-box">
|
||||||
|
<summary class="box-header">
|
||||||
|
<span><i class="bi bi-cash-coin me-2"></i> Valores e Datas</span>
|
||||||
|
<i class="bi bi-chevron-down ms-auto transition-icon"></i>
|
||||||
|
</summary>
|
||||||
|
<div class="box-body">
|
||||||
|
<div class="form-grid">
|
||||||
|
<div class="form-field"><label>Quantidade</label><input class="form-control form-control-sm" type="number" [(ngModel)]="controleEditModel.quantidade" (ngModelChange)="onControleEditValueChange()" /></div>
|
||||||
|
<div class="form-field"><label>Valor Unit</label><input class="form-control form-control-sm" type="number" [(ngModel)]="controleEditModel.valorUnit" (ngModelChange)="onControleEditValueChange()" /></div>
|
||||||
|
<div class="form-field"><label>Valor NF</label><input class="form-control form-control-sm" type="number" [(ngModel)]="controleEditModel.valorDaNf" (ngModelChange)="onControleEditValueChange()" /></div>
|
||||||
|
<div class="form-field"><label>Data NF</label><input class="form-control form-control-sm" type="date" [(ngModel)]="controleEditDataNf" (ngModelChange)="onControleEditDateChange()" /></div>
|
||||||
|
<div class="form-field"><label>Recebimento</label><input class="form-control form-control-sm" type="date" [(ngModel)]="controleEditRecebimento" /></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</details>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer p-3 text-end border-top bg-white">
|
||||||
|
<button class="btn btn-glass btn-sm me-2" (click)="closeControleEdit()">Cancelar</button>
|
||||||
|
<button class="btn btn-primary btn-sm" [disabled]="controleEditSaving" (click)="saveControleEdit()">
|
||||||
|
{{ controleEditSaving ? 'Salvando...' : 'Salvar' }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- MODAL CONTROLE DELETE -->
|
||||||
|
<div class="modal-custom" *ngIf="controleDeleteOpen">
|
||||||
|
<div class="modal-card modal-lg" (click)="$event.stopPropagation()">
|
||||||
|
<div class="modal-header">
|
||||||
|
<div class="modal-title">
|
||||||
|
<span class="icon-bg danger-soft"><i class="bi bi-trash"></i></span>
|
||||||
|
Remover Recebimento
|
||||||
|
</div>
|
||||||
|
<button class="btn btn-sm btn-icon" (click)="cancelControleDelete()"><i class="bi bi-x-lg"></i></button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body modern-body bg-light-gray">
|
||||||
|
<div class="confirm-delete">
|
||||||
|
<div class="confirm-icon"><i class="bi bi-trash"></i></div>
|
||||||
|
<p class="mb-0">Confirma remover a NF <strong>{{ controleDeleteTarget?.notaFiscal }}</strong>?</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer p-3 text-end border-top bg-white">
|
||||||
|
<button class="btn btn-glass btn-sm me-2" (click)="cancelControleDelete()">Cancelar</button>
|
||||||
|
<button class="btn btn-danger btn-sm" (click)="confirmControleDelete()">Excluir</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -616,6 +616,8 @@
|
||||||
/* ========================================================== */
|
/* ========================================================== */
|
||||||
/* MODAIS (mantidos) */
|
/* MODAIS (mantidos) */
|
||||||
/* ========================================================== */
|
/* ========================================================== */
|
||||||
|
.modal-backdrop-custom { position: fixed; inset: 0; background: rgba(0,0,0,0.45); z-index: 9990; backdrop-filter: blur(4px); }
|
||||||
|
.modal-custom { position: fixed; inset: 0; display: flex; align-items: center; justify-content: center; z-index: 9995; padding: 16px; }
|
||||||
.modal-card { background: #ffffff; border: 1px solid rgba(255,255,255,0.8); border-radius: 20px; box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25); overflow: hidden; display: flex; flex-direction: column; animation: modalPop 0.3s cubic-bezier(0.34, 1.56, 0.64, 1); width: min(850px, 100%); max-height: 90vh; min-height: 0; }
|
.modal-card { background: #ffffff; border: 1px solid rgba(255,255,255,0.8); border-radius: 20px; box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25); overflow: hidden; display: flex; flex-direction: column; animation: modalPop 0.3s cubic-bezier(0.34, 1.56, 0.64, 1); width: min(850px, 100%); max-height: 90vh; min-height: 0; }
|
||||||
.modal-card.modal-xl-custom { width: min(980px, 92vw); max-height: 82vh; }
|
.modal-card.modal-xl-custom { width: min(980px, 92vw); max-height: 82vh; }
|
||||||
.modal-card.modal-lg { width: min(720px, 92vw); max-height: 80vh; }
|
.modal-card.modal-lg { width: min(720px, 92vw); max-height: 80vh; }
|
||||||
|
|
|
||||||
|
|
@ -4,18 +4,8 @@ import { FormsModule } from '@angular/forms';
|
||||||
import { HttpClient } from '@angular/common/http';
|
import { HttpClient } from '@angular/common/http';
|
||||||
import { ChipsControleService, ChipVirgemListDto, ControleRecebidoListDto, SortDir, UpdateChipVirgemRequest, UpdateControleRecebidoRequest, CreateChipVirgemRequest, CreateControleRecebidoRequest } from '../../services/chips-controle.service';
|
import { ChipsControleService, ChipVirgemListDto, ControleRecebidoListDto, SortDir, UpdateChipVirgemRequest, UpdateControleRecebidoRequest, CreateChipVirgemRequest, CreateControleRecebidoRequest } from '../../services/chips-controle.service';
|
||||||
import { CustomSelectComponent } from '../../components/custom-select/custom-select';
|
import { CustomSelectComponent } from '../../components/custom-select/custom-select';
|
||||||
import { ChipsControleModalsComponent } from '../../components/page-modals/chips-controle-modals/chips-controle-modals';
|
|
||||||
import { AuthService } from '../../services/auth.service';
|
import { AuthService } from '../../services/auth.service';
|
||||||
import { TableExportService } from '../../services/table-export.service';
|
|
||||||
import { confirmDeletionWithTyping } from '../../utils/destructive-confirmation';
|
import { confirmDeletionWithTyping } from '../../utils/destructive-confirmation';
|
||||||
import {
|
|
||||||
buildPageNumbers,
|
|
||||||
clampPage,
|
|
||||||
computePageEnd,
|
|
||||||
computePageStart,
|
|
||||||
computeTotalPages
|
|
||||||
} from '../../utils/pagination.util';
|
|
||||||
import { firstValueFrom } from 'rxjs';
|
|
||||||
|
|
||||||
// Interface para o Agrupamento
|
// Interface para o Agrupamento
|
||||||
interface ChipGroup {
|
interface ChipGroup {
|
||||||
|
|
@ -45,12 +35,11 @@ type ControleSortKey =
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-chips-controle-recebidos',
|
selector: 'app-chips-controle-recebidos',
|
||||||
standalone: true,
|
standalone: true,
|
||||||
imports: [CommonModule, FormsModule, CustomSelectComponent, ChipsControleModalsComponent],
|
imports: [CommonModule, FormsModule, CustomSelectComponent],
|
||||||
templateUrl: './chips-controle-recebidos.html',
|
templateUrl: './chips-controle-recebidos.html',
|
||||||
styleUrls: ['./chips-controle-recebidos.scss']
|
styleUrls: ['./chips-controle-recebidos.scss']
|
||||||
})
|
})
|
||||||
export class ChipsControleRecebidos implements OnInit, OnDestroy {
|
export class ChipsControleRecebidos implements OnInit, OnDestroy {
|
||||||
readonly vm = this;
|
|
||||||
activeTab: 'chips' | 'controle' = 'chips';
|
activeTab: 'chips' | 'controle' = 'chips';
|
||||||
|
|
||||||
// --- Chips ---
|
// --- Chips ---
|
||||||
|
|
@ -97,7 +86,6 @@ export class ChipsControleRecebidos implements OnInit, OnDestroy {
|
||||||
toastOpen = false;
|
toastOpen = false;
|
||||||
toastMessage = '';
|
toastMessage = '';
|
||||||
toastType: 'success' | 'danger' = 'success';
|
toastType: 'success' | 'danger' = 'success';
|
||||||
exporting = false;
|
|
||||||
private toastTimer: any = null;
|
private toastTimer: any = null;
|
||||||
|
|
||||||
chipDetailOpen = false;
|
chipDetailOpen = false;
|
||||||
|
|
@ -136,8 +124,7 @@ export class ChipsControleRecebidos implements OnInit, OnDestroy {
|
||||||
@Inject(PLATFORM_ID) private platformId: object,
|
@Inject(PLATFORM_ID) private platformId: object,
|
||||||
private service: ChipsControleService,
|
private service: ChipsControleService,
|
||||||
private http: HttpClient,
|
private http: HttpClient,
|
||||||
private authService: AuthService,
|
private authService: AuthService
|
||||||
private tableExportService: TableExportService
|
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
|
|
@ -430,129 +417,6 @@ export class ChipsControleRecebidos implements OnInit, OnDestroy {
|
||||||
this.fetchControle();
|
this.fetchControle();
|
||||||
}
|
}
|
||||||
|
|
||||||
async onExport(): Promise<void> {
|
|
||||||
if (this.exporting) return;
|
|
||||||
this.exporting = true;
|
|
||||||
|
|
||||||
try {
|
|
||||||
if (this.activeTab === 'chips') {
|
|
||||||
const baseRows = [...(this.chipsRows ?? [])].sort((a, b) => (a.item ?? 0) - (b.item ?? 0));
|
|
||||||
const rows = await this.fetchDetailedChipRowsForExport(baseRows);
|
|
||||||
if (!rows.length) {
|
|
||||||
this.showToast('Nenhum registro encontrado para exportar.', 'danger');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const timestamp = this.tableExportService.buildTimestamp();
|
|
||||||
await this.tableExportService.exportAsXlsx<ChipVirgemListDto>({
|
|
||||||
fileName: `chips_virgens_${timestamp}`,
|
|
||||||
sheetName: 'ChipsVirgens',
|
|
||||||
rows,
|
|
||||||
columns: [
|
|
||||||
{ header: 'ID', value: (row) => row.id ?? '' },
|
|
||||||
{ header: 'Item', type: 'number', value: (row) => this.toNullableNumber(row.item) ?? 0 },
|
|
||||||
{ header: 'Numero do Chip', value: (row) => row.numeroDoChip ?? '' },
|
|
||||||
{ header: 'Observacoes', value: (row) => row.observacoes ?? '' },
|
|
||||||
{ header: 'Criado Em', type: 'datetime', value: (row) => row.createdAt ?? '' },
|
|
||||||
{ header: 'Atualizado Em', type: 'datetime', value: (row) => row.updatedAt ?? '' },
|
|
||||||
],
|
|
||||||
});
|
|
||||||
|
|
||||||
this.showToast(`Planilha exportada com ${rows.length} registro(s).`, 'success');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const baseRows = [...(this.controleRows ?? [])].sort((a, b) => {
|
|
||||||
const byAno = (this.toNullableNumber(a.ano) ?? 0) - (this.toNullableNumber(b.ano) ?? 0);
|
|
||||||
if (byAno !== 0) return byAno;
|
|
||||||
return (this.toNullableNumber(a.item) ?? 0) - (this.toNullableNumber(b.item) ?? 0);
|
|
||||||
});
|
|
||||||
const rows = await this.fetchDetailedControleRowsForExport(baseRows);
|
|
||||||
|
|
||||||
if (!rows.length) {
|
|
||||||
this.showToast('Nenhum registro encontrado para exportar.', 'danger');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const timestamp = this.tableExportService.buildTimestamp();
|
|
||||||
await this.tableExportService.exportAsXlsx<ControleRecebidoListDto>({
|
|
||||||
fileName: `controle_recebidos_${timestamp}`,
|
|
||||||
sheetName: 'ControleRecebidos',
|
|
||||||
rows,
|
|
||||||
columns: [
|
|
||||||
{ header: 'ID', value: (row) => row.id ?? '' },
|
|
||||||
{ header: 'Ano', type: 'number', value: (row) => this.toNullableNumber(row.ano) ?? 0 },
|
|
||||||
{ header: 'Item', type: 'number', value: (row) => this.toNullableNumber(row.item) ?? 0 },
|
|
||||||
{ header: 'Nota Fiscal', value: (row) => row.notaFiscal ?? '' },
|
|
||||||
{ header: 'Chip', value: (row) => row.chip ?? '' },
|
|
||||||
{ header: 'Serial', value: (row) => row.serial ?? '' },
|
|
||||||
{ header: 'Conteudo da NF', value: (row) => row.conteudoDaNf ?? '' },
|
|
||||||
{ header: 'Numero da Linha', value: (row) => row.numeroDaLinha ?? '' },
|
|
||||||
{ header: 'Valor Unitario', type: 'currency', value: (row) => this.toNullableNumber(row.valorUnit) ?? 0 },
|
|
||||||
{ header: 'Valor da NF', type: 'currency', value: (row) => this.toNullableNumber(row.valorDaNf) ?? 0 },
|
|
||||||
{ header: 'Data da NF', type: 'date', value: (row) => row.dataDaNf ?? '' },
|
|
||||||
{ header: 'Data do Recebimento', type: 'date', value: (row) => row.dataDoRecebimento ?? '' },
|
|
||||||
{ header: 'Quantidade', type: 'number', value: (row) => this.toNullableNumber(row.quantidade) ?? 0 },
|
|
||||||
{ header: 'Resumo', type: 'boolean', value: (row) => !!row.isResumo },
|
|
||||||
{ header: 'Criado Em', type: 'datetime', value: (row) => row.createdAt ?? '' },
|
|
||||||
{ header: 'Atualizado Em', type: 'datetime', value: (row) => row.updatedAt ?? '' },
|
|
||||||
],
|
|
||||||
});
|
|
||||||
|
|
||||||
this.showToast(`Planilha exportada com ${rows.length} registro(s).`, 'success');
|
|
||||||
} catch {
|
|
||||||
this.showToast('Erro ao exportar planilha.', 'danger');
|
|
||||||
} finally {
|
|
||||||
this.exporting = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async fetchDetailedChipRowsForExport(rows: ChipVirgemListDto[]): Promise<ChipVirgemListDto[]> {
|
|
||||||
if (!rows.length) return [];
|
|
||||||
|
|
||||||
const detailed: ChipVirgemListDto[] = [];
|
|
||||||
const chunkSize = 12;
|
|
||||||
|
|
||||||
for (let i = 0; i < rows.length; i += chunkSize) {
|
|
||||||
const chunk = rows.slice(i, i + chunkSize);
|
|
||||||
const resolved = await Promise.all(
|
|
||||||
chunk.map(async (row) => {
|
|
||||||
try {
|
|
||||||
return await firstValueFrom(this.service.getChipVirgemById(row.id));
|
|
||||||
} catch {
|
|
||||||
return row;
|
|
||||||
}
|
|
||||||
})
|
|
||||||
);
|
|
||||||
detailed.push(...resolved);
|
|
||||||
}
|
|
||||||
|
|
||||||
return detailed;
|
|
||||||
}
|
|
||||||
|
|
||||||
private async fetchDetailedControleRowsForExport(rows: ControleRecebidoListDto[]): Promise<ControleRecebidoListDto[]> {
|
|
||||||
if (!rows.length) return [];
|
|
||||||
|
|
||||||
const detailed: ControleRecebidoListDto[] = [];
|
|
||||||
const chunkSize = 12;
|
|
||||||
|
|
||||||
for (let i = 0; i < rows.length; i += chunkSize) {
|
|
||||||
const chunk = rows.slice(i, i + chunkSize);
|
|
||||||
const resolved = await Promise.all(
|
|
||||||
chunk.map(async (row) => {
|
|
||||||
try {
|
|
||||||
return await firstValueFrom(this.service.getControleRecebidoById(row.id));
|
|
||||||
} catch {
|
|
||||||
return row;
|
|
||||||
}
|
|
||||||
})
|
|
||||||
);
|
|
||||||
detailed.push(...resolved);
|
|
||||||
}
|
|
||||||
|
|
||||||
return detailed;
|
|
||||||
}
|
|
||||||
|
|
||||||
setControleSort(key: ControleSortKey) {
|
setControleSort(key: ControleSortKey) {
|
||||||
if (this.controleSortBy === key) {
|
if (this.controleSortBy === key) {
|
||||||
this.controleSortDir = this.controleSortDir === 'asc' ? 'desc' : 'asc';
|
this.controleSortDir = this.controleSortDir === 'asc' ? 'desc' : 'asc';
|
||||||
|
|
@ -856,19 +720,27 @@ export class ChipsControleRecebidos implements OnInit, OnDestroy {
|
||||||
get activePage() { return this.activeTab === 'chips' ? this.chipsPage : this.controlePage; }
|
get activePage() { return this.activeTab === 'chips' ? this.chipsPage : this.controlePage; }
|
||||||
get activeTotal() { return this.activeTab === 'chips' ? this.chipsTotal : this.controleTotal; }
|
get activeTotal() { return this.activeTab === 'chips' ? this.chipsTotal : this.controleTotal; }
|
||||||
get activePageSize() { return this.activeTab === 'chips' ? this.chipsPageSize : this.controlePageSize; }
|
get activePageSize() { return this.activeTab === 'chips' ? this.chipsPageSize : this.controlePageSize; }
|
||||||
get activeTotalPages() { return computeTotalPages(this.activeTotal || 0, this.activePageSize || 10); }
|
get activeTotalPages() { return Math.max(1, Math.ceil((this.activeTotal || 0) / (this.activePageSize || 10))); }
|
||||||
|
|
||||||
get activePageStart() { return computePageStart(this.activeTotal || 0, this.activePage, this.activePageSize); }
|
get activePageStart() { return this.activeTotal === 0 ? 0 : (this.activePage - 1) * this.activePageSize + 1; }
|
||||||
get activePageEnd() { return computePageEnd(this.activeTotal || 0, this.activePage, this.activePageSize); }
|
get activePageEnd() { return this.activeTotal === 0 ? 0 : Math.min(this.activePage * this.activePageSize, this.activeTotal); }
|
||||||
|
|
||||||
get activeLoading() { return this.activeTab === 'chips' ? this.chipsLoading : this.controleLoading; } // ✅ novo
|
get activeLoading() { return this.activeTab === 'chips' ? this.chipsLoading : this.controleLoading; } // ✅ novo
|
||||||
|
|
||||||
get activePageNumbers() {
|
get activePageNumbers() {
|
||||||
return buildPageNumbers(this.activePage, this.activeTotalPages);
|
const total = this.activeTotalPages;
|
||||||
|
const current = this.activePage;
|
||||||
|
const max = 5;
|
||||||
|
let start = Math.max(1, current - 2);
|
||||||
|
let end = Math.min(total, start + (max - 1));
|
||||||
|
start = Math.max(1, end - (max - 1));
|
||||||
|
const pages = [];
|
||||||
|
for (let i = start; i <= end; i++) pages.push(i);
|
||||||
|
return pages;
|
||||||
}
|
}
|
||||||
|
|
||||||
goToPage(p: number) {
|
goToPage(p: number) {
|
||||||
const target = clampPage(p, this.activeTotalPages);
|
const target = Math.max(1, Math.min(this.activeTotalPages, p));
|
||||||
|
|
||||||
if (this.activeTab === 'chips') {
|
if (this.activeTab === 'chips') {
|
||||||
this.chipsPage = target;
|
this.chipsPage = target;
|
||||||
|
|
|
||||||
|
|
@ -32,10 +32,6 @@
|
||||||
<button type="button" class="btn btn-brand btn-sm" (click)="refresh()" [disabled]="loading">
|
<button type="button" class="btn btn-brand btn-sm" (click)="refresh()" [disabled]="loading">
|
||||||
<i class="bi bi-arrow-clockwise me-1"></i> Atualizar
|
<i class="bi bi-arrow-clockwise me-1"></i> Atualizar
|
||||||
</button>
|
</button>
|
||||||
<button type="button" class="btn btn-glass btn-sm" (click)="onExport()" [disabled]="loading || exporting">
|
|
||||||
<span *ngIf="!exporting"><i class="bi bi-download me-1"></i> Exportar</span>
|
|
||||||
<span *ngIf="exporting"><span class="spinner-border spinner-border-sm me-2"></span> Exportando...</span>
|
|
||||||
</button>
|
|
||||||
<button *ngIf="isSysAdmin" type="button" class="btn btn-brand btn-sm" (click)="openCreate()">
|
<button *ngIf="isSysAdmin" type="button" class="btn btn-brand btn-sm" (click)="openCreate()">
|
||||||
<i class="bi bi-plus-circle me-1"></i> Novo Usuário
|
<i class="bi bi-plus-circle me-1"></i> Novo Usuário
|
||||||
</button>
|
</button>
|
||||||
|
|
@ -92,6 +88,7 @@
|
||||||
<span class="text-muted small fw-bold text-uppercase" style="letter-spacing: 0.5px; font-size: 0.75rem;">Itens por pág:</span>
|
<span class="text-muted small fw-bold text-uppercase" style="letter-spacing: 0.5px; font-size: 0.75rem;">Itens por pág:</span>
|
||||||
<div class="select-wrapper">
|
<div class="select-wrapper">
|
||||||
<app-select class="select-glass" size="sm" [options]="pageSizeOptions" [(ngModel)]="pageSize" (ngModelChange)="onPageSizeChange()" [disabled]="loading"></app-select>
|
<app-select class="select-glass" size="sm" [options]="pageSizeOptions" [(ngModel)]="pageSize" (ngModelChange)="onPageSizeChange()" [disabled]="loading"></app-select>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -184,4 +181,278 @@
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<app-dados-usuarios-modals [vm]="$any(vm)"></app-dados-usuarios-modals>
|
<div class="modal-backdrop-custom" *ngIf="detailsOpen || editOpen || deleteOpen || createOpen" (click)="closeDetails(); closeEdit(); cancelDelete(); closeCreate()"></div>
|
||||||
|
<div class="modal-custom" *ngIf="detailsOpen || editOpen || deleteOpen || createOpen" (click)="closeDetails(); closeEdit(); cancelDelete(); closeCreate()">
|
||||||
|
<div *ngIf="detailsOpen" class="modal-card modal-lg" (click)="$event.stopPropagation()">
|
||||||
|
<div class="modal-header">
|
||||||
|
<div class="modal-title">
|
||||||
|
<span class="icon-bg primary-soft"><i class="bi bi-person-vcard"></i></span>
|
||||||
|
Detalhes do Usuário
|
||||||
|
</div>
|
||||||
|
<button class="btn-icon" (click)="closeDetails()"><i class="bi bi-x-lg"></i></button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body modern-body bg-light-gray">
|
||||||
|
<div class="details-dashboard">
|
||||||
|
<div class="detail-box w-100">
|
||||||
|
<div class="box-header"><span><i class="bi bi-card-text me-2"></i> Informações</span></div>
|
||||||
|
<div class="box-body">
|
||||||
|
<div class="form-grid">
|
||||||
|
<div class="form-field span-2"><label>CLIENTE</label><div class="fw-bold">{{ selectedRow?.cliente }}</div></div>
|
||||||
|
<div class="form-field"><label>TIPO</label><div>{{ (selectedRow?.tipoPessoa || 'PF') === 'PJ' ? 'PESSOA JURÍDICA' : 'PESSOA FÍSICA' }}</div></div>
|
||||||
|
<div class="form-field span-2"><label>{{ (selectedRow?.tipoPessoa || 'PF') === 'PJ' ? 'RAZÃO SOCIAL' : 'NOME' }}</label><div>{{ (selectedRow?.tipoPessoa || 'PF') === 'PJ' ? (selectedRow?.razaoSocial || selectedRow?.cliente || '-') : (selectedRow?.nome || selectedRow?.cliente || '-') }}</div></div>
|
||||||
|
<div class="form-field"><label>LINHA</label><div class="fw-black text-blue fs-5">{{ selectedRow?.linha }}</div></div>
|
||||||
|
<div class="form-field"><label>ITEM</label><div>{{ selectedRow?.item }}</div></div>
|
||||||
|
<div class="form-field"><label>{{ (selectedRow?.tipoPessoa || 'PF') === 'PJ' ? 'CNPJ' : 'CPF' }}</label><div>{{ (selectedRow?.tipoPessoa || 'PF') === 'PJ' ? (selectedRow?.cnpj || '-') : (selectedRow?.cpf || '-') }}</div></div>
|
||||||
|
<div class="form-field"><label>RG</label><div>{{ selectedRow?.rg || '-' }}</div></div>
|
||||||
|
|
||||||
|
<div class="form-field span-2"><label>E-MAIL</label><div>{{ selectedRow?.email || '-' }}</div></div>
|
||||||
|
|
||||||
|
<div class="form-field"><label>CELULAR</label><div>{{ selectedRow?.celular || '-' }}</div></div>
|
||||||
|
<div class="form-field"><label>TELEFONE FIXO</label><div>{{ selectedRow?.telefoneFixo || '-' }}</div></div>
|
||||||
|
|
||||||
|
<div class="form-field span-2"><label>ENDEREÇO</label><div>{{ selectedRow?.endereco || '-' }}</div></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- CREATE MODAL -->
|
||||||
|
<div *ngIf="createOpen" class="modal-card modal-xl-custom create-modal" (click)="$event.stopPropagation()">
|
||||||
|
<div class="modal-header">
|
||||||
|
<div class="modal-title">
|
||||||
|
<span class="icon-bg primary-soft"><i class="bi bi-plus-circle"></i></span>
|
||||||
|
Novo Usuário
|
||||||
|
</div>
|
||||||
|
<button class="btn-icon" (click)="closeCreate()"><i class="bi bi-x-lg"></i></button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body modern-body bg-light-gray" *ngIf="createModel">
|
||||||
|
<div class="edit-sections">
|
||||||
|
<details open class="detail-box">
|
||||||
|
<summary class="box-header">
|
||||||
|
<span><i class="bi bi-link-45deg me-2"></i> Vínculo com Reserva</span>
|
||||||
|
<i class="bi bi-chevron-down ms-auto transition-icon"></i>
|
||||||
|
</summary>
|
||||||
|
<div class="box-body">
|
||||||
|
<div class="form-grid">
|
||||||
|
<div class="form-field span-2">
|
||||||
|
<label>Linha (RESERVA)</label>
|
||||||
|
<app-select
|
||||||
|
class="form-select"
|
||||||
|
size="sm"
|
||||||
|
[options]="lineOptionsCreate"
|
||||||
|
labelKey="label"
|
||||||
|
valueKey="id"
|
||||||
|
[searchable]="true"
|
||||||
|
searchPlaceholder="Pesquisar linha da reserva..."
|
||||||
|
[(ngModel)]="createModel.mobileLineId"
|
||||||
|
(ngModelChange)="onCreateLineChange()"
|
||||||
|
[disabled]="createLinesLoading"
|
||||||
|
placeholder="Selecione uma linha da Reserva..."
|
||||||
|
></app-select>
|
||||||
|
<small class="field-hint" *ngIf="createLinesLoading">Carregando linhas da Reserva...</small>
|
||||||
|
</div>
|
||||||
|
<div class="form-field">
|
||||||
|
<label>Total Franquia Line</label>
|
||||||
|
<input class="form-control form-control-sm bg-light" [value]="formatFranquiaLine(createFranquiaLineTotal)" readonly />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</details>
|
||||||
|
|
||||||
|
<details open class="detail-box">
|
||||||
|
<summary class="box-header">
|
||||||
|
<span><i class="bi bi-person-vcard me-2"></i> Dados do Usuário</span>
|
||||||
|
<i class="bi bi-chevron-down ms-auto transition-icon"></i>
|
||||||
|
</summary>
|
||||||
|
<div class="box-body">
|
||||||
|
<div class="form-grid user-modal-grid">
|
||||||
|
<div class="form-field span-2"><label>Cliente</label><input class="form-control form-control-sm" [(ngModel)]="createModel.cliente" /></div>
|
||||||
|
<div class="form-field field-tipo"><label>Tipo</label>
|
||||||
|
<app-select
|
||||||
|
class="form-select"
|
||||||
|
size="sm"
|
||||||
|
[options]="tipoPessoaOptions"
|
||||||
|
labelKey="label"
|
||||||
|
valueKey="value"
|
||||||
|
[(ngModel)]="createModel.tipoPessoa"
|
||||||
|
(ngModelChange)="onCreateTipoChange()">
|
||||||
|
</app-select>
|
||||||
|
</div>
|
||||||
|
<div class="form-field span-2" *ngIf="(createModel.tipoPessoa || 'PF') === 'PF'">
|
||||||
|
<label>Nome</label>
|
||||||
|
<input class="form-control form-control-sm" [(ngModel)]="createModel.nome" />
|
||||||
|
</div>
|
||||||
|
<div class="form-field span-2" *ngIf="(createModel.tipoPessoa || 'PF') === 'PJ'">
|
||||||
|
<label>Razão Social</label>
|
||||||
|
<input class="form-control form-control-sm" [(ngModel)]="createModel.razaoSocial" />
|
||||||
|
</div>
|
||||||
|
<div class="form-field field-line">
|
||||||
|
<label>Linha</label>
|
||||||
|
<input class="form-control form-control-sm bg-light" [value]="createModel.linha || ''" readonly />
|
||||||
|
</div>
|
||||||
|
<div class="form-field field-item field-auto">
|
||||||
|
<label>Item (Automático)</label>
|
||||||
|
<input class="form-control form-control-sm bg-light" type="number" [(ngModel)]="createModel.item" readonly title="Gerado automaticamente pelo sistema" />
|
||||||
|
<small class="field-hint">Gerado automaticamente pelo sistema</small>
|
||||||
|
</div>
|
||||||
|
<div class="form-field field-cpf-cnpj" *ngIf="(createModel.tipoPessoa || 'PF') === 'PF'"><label>CPF</label><input class="form-control form-control-sm" [(ngModel)]="createModel.cpf" /></div>
|
||||||
|
<div class="form-field field-cpf-cnpj" *ngIf="(createModel.tipoPessoa || 'PF') === 'PJ'"><label>CNPJ</label><input class="form-control form-control-sm" [(ngModel)]="createModel.cnpj" /></div>
|
||||||
|
<div class="form-field field-rg"><label>RG</label><input class="form-control form-control-sm" [(ngModel)]="createModel.rg" /></div>
|
||||||
|
<div class="form-field span-2"><label>E-mail</label><input class="form-control form-control-sm" [(ngModel)]="createModel.email" /></div>
|
||||||
|
<div class="form-field span-2"><label>Endereço</label><input class="form-control form-control-sm" [(ngModel)]="createModel.endereco" /></div>
|
||||||
|
<div class="form-field field-celular"><label>Celular</label><input class="form-control form-control-sm" [(ngModel)]="createModel.celular" /></div>
|
||||||
|
<div class="form-field field-telefone"><label>Telefone Fixo</label><input class="form-control form-control-sm" [(ngModel)]="createModel.telefoneFixo" /></div>
|
||||||
|
<div class="form-field span-2" *ngIf="(createModel.tipoPessoa || 'PF') === 'PF'"><label>Data de Nascimento</label><input class="form-control form-control-sm" type="date" [(ngModel)]="createDateNascimento" /></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</details>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="modal-footer p-3 text-end border-top bg-white">
|
||||||
|
<button class="btn btn-glass btn-sm me-2" (click)="closeCreate()">Cancelar</button>
|
||||||
|
<button class="btn btn-brand btn-sm" [disabled]="createSaving" (click)="saveCreate()">
|
||||||
|
{{ createSaving ? 'Salvando...' : 'Salvar' }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- EDIT MODAL -->
|
||||||
|
<div *ngIf="editOpen" class="modal-card modal-xl-custom" (click)="$event.stopPropagation()">
|
||||||
|
<div class="modal-header">
|
||||||
|
<div class="modal-title">
|
||||||
|
<span class="icon-bg primary-soft"><i class="bi bi-pencil-square"></i></span>
|
||||||
|
Editar Usuário
|
||||||
|
</div>
|
||||||
|
<button class="btn-icon" (click)="closeEdit()"><i class="bi bi-x-lg"></i></button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body modern-body bg-light-gray" *ngIf="editModel">
|
||||||
|
<div class="edit-sections">
|
||||||
|
<details open class="detail-box">
|
||||||
|
<summary class="box-header">
|
||||||
|
<span><i class="bi bi-person-badge me-2"></i> Identificação</span>
|
||||||
|
<i class="bi bi-chevron-down ms-auto transition-icon"></i>
|
||||||
|
</summary>
|
||||||
|
<div class="box-body">
|
||||||
|
<div class="form-grid user-modal-grid">
|
||||||
|
<div class="form-field span-2"><label>Cliente</label><input class="form-control form-control-sm" [(ngModel)]="editModel.cliente" /></div>
|
||||||
|
<div class="form-field field-tipo"><label>Tipo</label>
|
||||||
|
<app-select
|
||||||
|
class="form-select"
|
||||||
|
size="sm"
|
||||||
|
[options]="tipoPessoaOptions"
|
||||||
|
labelKey="label"
|
||||||
|
valueKey="value"
|
||||||
|
[(ngModel)]="editModel.tipoPessoa"
|
||||||
|
(ngModelChange)="onEditTipoChange()">
|
||||||
|
</app-select>
|
||||||
|
</div>
|
||||||
|
<div class="form-field span-2" *ngIf="(editModel.tipoPessoa || 'PF') === 'PF'">
|
||||||
|
<label>Nome</label>
|
||||||
|
<input class="form-control form-control-sm" [(ngModel)]="editModel.nome" />
|
||||||
|
</div>
|
||||||
|
<div class="form-field span-2" *ngIf="(editModel.tipoPessoa || 'PF') === 'PJ'">
|
||||||
|
<label>Razão Social</label>
|
||||||
|
<input class="form-control form-control-sm" [(ngModel)]="editModel.razaoSocial" />
|
||||||
|
</div>
|
||||||
|
<div class="form-field field-line">
|
||||||
|
<label>Linha (Reserva)</label>
|
||||||
|
<app-select
|
||||||
|
class="form-select"
|
||||||
|
size="sm"
|
||||||
|
[options]="editLineOptions"
|
||||||
|
labelKey="label"
|
||||||
|
valueKey="id"
|
||||||
|
[searchable]="true"
|
||||||
|
searchPlaceholder="Pesquisar linha da reserva..."
|
||||||
|
[(ngModel)]="editSelectedLineId"
|
||||||
|
(ngModelChange)="onEditLineChange()"
|
||||||
|
[disabled]="createLinesLoading"
|
||||||
|
placeholder="Selecione uma linha da Reserva..."
|
||||||
|
></app-select>
|
||||||
|
</div>
|
||||||
|
<div class="form-field field-auto">
|
||||||
|
<label>Total Franquia Line</label>
|
||||||
|
<input class="form-control form-control-sm bg-light" [value]="formatFranquiaLine(editFranquiaLineTotal)" readonly />
|
||||||
|
</div>
|
||||||
|
<div class="form-field field-item field-auto">
|
||||||
|
<label>Item (Automático)</label>
|
||||||
|
<input class="form-control form-control-sm bg-light" type="number" [(ngModel)]="editModel.item" readonly title="Gerado automaticamente pelo sistema" />
|
||||||
|
<small class="field-hint">Gerado automaticamente pelo sistema</small>
|
||||||
|
</div>
|
||||||
|
<div class="form-field field-cpf-cnpj" *ngIf="(editModel.tipoPessoa || 'PF') === 'PF'">
|
||||||
|
<label>CPF</label>
|
||||||
|
<input class="form-control form-control-sm" [(ngModel)]="editModel.cpf" />
|
||||||
|
</div>
|
||||||
|
<div class="form-field field-cpf-cnpj" *ngIf="(editModel.tipoPessoa || 'PF') === 'PJ'">
|
||||||
|
<label>CNPJ</label>
|
||||||
|
<input class="form-control form-control-sm" [(ngModel)]="editModel.cnpj" />
|
||||||
|
</div>
|
||||||
|
<div class="form-field field-rg"><label>RG</label><input class="form-control form-control-sm" [(ngModel)]="editModel.rg" /></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</details>
|
||||||
|
|
||||||
|
<details open class="detail-box">
|
||||||
|
<summary class="box-header">
|
||||||
|
<span><i class="bi bi-envelope-paper me-2"></i> Contato</span>
|
||||||
|
<i class="bi bi-chevron-down ms-auto transition-icon"></i>
|
||||||
|
</summary>
|
||||||
|
<div class="box-body">
|
||||||
|
<div class="form-grid user-modal-grid contact-modal-grid">
|
||||||
|
<div class="form-field span-2"><label>E-mail</label><input class="form-control form-control-sm" type="email" [(ngModel)]="editModel.email" /></div>
|
||||||
|
<div class="form-field field-celular"><label>Celular</label><input class="form-control form-control-sm" [(ngModel)]="editModel.celular" /></div>
|
||||||
|
<div class="form-field field-telefone"><label>Telefone Fixo</label><input class="form-control form-control-sm" [(ngModel)]="editModel.telefoneFixo" /></div>
|
||||||
|
<div class="form-field span-2"><label>Endereço</label><input class="form-control form-control-sm" [(ngModel)]="editModel.endereco" /></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</details>
|
||||||
|
|
||||||
|
<details open class="detail-box">
|
||||||
|
<summary class="box-header">
|
||||||
|
<span><i class="bi bi-calendar-event me-2"></i> Complemento</span>
|
||||||
|
<i class="bi bi-chevron-down ms-auto transition-icon"></i>
|
||||||
|
</summary>
|
||||||
|
<div class="box-body">
|
||||||
|
<div class="form-grid">
|
||||||
|
<div class="form-field" *ngIf="(editModel.tipoPessoa || 'PF') === 'PF'">
|
||||||
|
<label>Data Nascimento</label>
|
||||||
|
<input class="form-control form-control-sm" type="date" [(ngModel)]="editDateNascimento" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</details>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer p-3 text-end border-top bg-white">
|
||||||
|
<button class="btn btn-glass btn-sm me-2" (click)="closeEdit()">Cancelar</button>
|
||||||
|
<button class="btn btn-primary btn-sm" [disabled]="editSaving" (click)="saveEdit()">
|
||||||
|
{{ editSaving ? 'Salvando...' : 'Salvar' }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- DELETE MODAL -->
|
||||||
|
<div *ngIf="deleteOpen" class="modal-card modal-lg" (click)="$event.stopPropagation()">
|
||||||
|
<div class="modal-header">
|
||||||
|
<div class="modal-title">
|
||||||
|
<span class="icon-bg danger-soft"><i class="bi bi-trash"></i></span>
|
||||||
|
Remover Usuário
|
||||||
|
</div>
|
||||||
|
<button class="btn-icon" (click)="cancelDelete()"><i class="bi bi-x-lg"></i></button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body modern-body bg-light-gray">
|
||||||
|
<div class="confirm-delete">
|
||||||
|
<div class="confirm-icon"><i class="bi bi-trash"></i></div>
|
||||||
|
<p class="mb-0">Confirma remover o registro <strong>{{ deleteTarget?.linha }}</strong>?</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer p-3 text-end border-top bg-white">
|
||||||
|
<button class="btn btn-glass btn-sm me-2" (click)="cancelDelete()">Cancelar</button>
|
||||||
|
<button class="btn btn-danger btn-sm" (click)="confirmDelete()">Excluir</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -350,6 +350,8 @@
|
||||||
.pagination-modern .page-item.active .page-link { background-color: var(--blue); border-color: var(--blue); color: #fff; }
|
.pagination-modern .page-item.active .page-link { background-color: var(--blue); border-color: var(--blue); color: #fff; }
|
||||||
|
|
||||||
/* MODALS */
|
/* MODALS */
|
||||||
|
.modal-backdrop-custom { position: fixed; inset: 0; background: rgba(0,0,0,0.45); z-index: 9990; backdrop-filter: blur(4px); }
|
||||||
|
.modal-custom { position: fixed; inset: 0; display: flex; align-items: center; justify-content: center; z-index: 9995; padding: clamp(12px, 2.2vw, 20px); }
|
||||||
.modal-card { background: #ffffff; border: 1px solid rgba(255,255,255,0.8); border-radius: 20px; box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25); overflow: hidden; display: flex; flex-direction: column; animation: modalPop 0.3s cubic-bezier(0.34, 1.56, 0.64, 1); width: min(850px, 100%); max-height: 90vh; min-height: 0; }
|
.modal-card { background: #ffffff; border: 1px solid rgba(255,255,255,0.8); border-radius: 20px; box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25); overflow: hidden; display: flex; flex-direction: column; animation: modalPop 0.3s cubic-bezier(0.34, 1.56, 0.64, 1); width: min(850px, 100%); max-height: 90vh; min-height: 0; }
|
||||||
.modal-xl-custom { width: min(1050px, 95vw); max-height: 86vh; }
|
.modal-xl-custom { width: min(1050px, 95vw); max-height: 86vh; }
|
||||||
@keyframes modalPop { from { opacity: 0; transform: scale(0.95) translateY(10px); } to { opacity: 1; transform: scale(1) translateY(0); } }
|
@keyframes modalPop { from { opacity: 0; transform: scale(0.95) translateY(10px); } to { opacity: 1; transform: scale(1) translateY(0); } }
|
||||||
|
|
|
||||||
|
|
@ -2,10 +2,7 @@ import { Component, OnInit, ViewChild, ElementRef } from '@angular/core';
|
||||||
import { CommonModule } from '@angular/common';
|
import { CommonModule } from '@angular/common';
|
||||||
import { FormsModule } from '@angular/forms';
|
import { FormsModule } from '@angular/forms';
|
||||||
import { HttpErrorResponse } from '@angular/common/http';
|
import { HttpErrorResponse } from '@angular/common/http';
|
||||||
import { firstValueFrom } from 'rxjs';
|
|
||||||
import { CustomSelectComponent } from '../../components/custom-select/custom-select';
|
import { CustomSelectComponent } from '../../components/custom-select/custom-select';
|
||||||
import { DadosUsuariosModalsComponent } from '../../components/page-modals/dados-usuarios-modals/dados-usuarios-modals';
|
|
||||||
import { TableExportService } from '../../services/table-export.service';
|
|
||||||
|
|
||||||
import {
|
import {
|
||||||
DadosUsuariosService,
|
DadosUsuariosService,
|
||||||
|
|
@ -19,13 +16,6 @@ import {
|
||||||
import { AuthService } from '../../services/auth.service';
|
import { AuthService } from '../../services/auth.service';
|
||||||
import { LinesService, MobileLineDetail } from '../../services/lines.service';
|
import { LinesService, MobileLineDetail } from '../../services/lines.service';
|
||||||
import { confirmDeletionWithTyping } from '../../utils/destructive-confirmation';
|
import { confirmDeletionWithTyping } from '../../utils/destructive-confirmation';
|
||||||
import {
|
|
||||||
buildPageNumbers,
|
|
||||||
clampPage,
|
|
||||||
computePageEnd,
|
|
||||||
computePageStart,
|
|
||||||
computeTotalPages
|
|
||||||
} from '../../utils/pagination.util';
|
|
||||||
|
|
||||||
type ViewMode = 'lines' | 'groups';
|
type ViewMode = 'lines' | 'groups';
|
||||||
|
|
||||||
|
|
@ -46,17 +36,15 @@ interface SimpleOption {
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-dados-usuarios',
|
selector: 'app-dados-usuarios',
|
||||||
standalone: true,
|
standalone: true,
|
||||||
imports: [CommonModule, FormsModule, CustomSelectComponent, DadosUsuariosModalsComponent],
|
imports: [CommonModule, FormsModule, CustomSelectComponent],
|
||||||
templateUrl: './dados-usuarios.html',
|
templateUrl: './dados-usuarios.html',
|
||||||
styleUrls: ['./dados-usuarios.scss']
|
styleUrls: ['./dados-usuarios.scss']
|
||||||
})
|
})
|
||||||
export class DadosUsuarios implements OnInit {
|
export class DadosUsuarios implements OnInit {
|
||||||
readonly vm = this;
|
|
||||||
|
|
||||||
@ViewChild('successToast', { static: false }) successToast!: ElementRef;
|
@ViewChild('successToast', { static: false }) successToast!: ElementRef;
|
||||||
|
|
||||||
loading = false;
|
loading = false;
|
||||||
exporting = false;
|
|
||||||
errorMsg = '';
|
errorMsg = '';
|
||||||
|
|
||||||
// Filtros
|
// Filtros
|
||||||
|
|
@ -128,8 +116,7 @@ export class DadosUsuarios implements OnInit {
|
||||||
constructor(
|
constructor(
|
||||||
private service: DadosUsuariosService,
|
private service: DadosUsuariosService,
|
||||||
private authService: AuthService,
|
private authService: AuthService,
|
||||||
private linesService: LinesService,
|
private linesService: LinesService
|
||||||
private tableExportService: TableExportService
|
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
|
|
@ -149,17 +136,26 @@ export class DadosUsuarios implements OnInit {
|
||||||
}
|
}
|
||||||
|
|
||||||
get totalPages(): number {
|
get totalPages(): number {
|
||||||
return computeTotalPages(this.total || 0, this.pageSize || 10);
|
return Math.max(1, Math.ceil((this.total || 0) / (this.pageSize || 10)));
|
||||||
}
|
}
|
||||||
|
|
||||||
get pageStart(): number { return computePageStart(this.total || 0, this.page, this.pageSize); }
|
get pageStart(): number { return (this.page - 1) * this.pageSize + 1; }
|
||||||
|
|
||||||
get pageEnd(): number {
|
get pageEnd(): number {
|
||||||
return computePageEnd(this.total || 0, this.page, this.pageSize);
|
const end = this.page * this.pageSize;
|
||||||
|
return end > this.total ? this.total : end;
|
||||||
}
|
}
|
||||||
|
|
||||||
get pageNumbers(): number[] {
|
get pageNumbers(): number[] {
|
||||||
return buildPageNumbers(this.page, this.totalPages);
|
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++) pages.push(i);
|
||||||
|
return pages;
|
||||||
}
|
}
|
||||||
|
|
||||||
fetch(goToPage?: number): void {
|
fetch(goToPage?: number): void {
|
||||||
|
|
@ -261,119 +257,13 @@ export class DadosUsuarios implements OnInit {
|
||||||
|
|
||||||
clearFilters() { this.search = ''; this.fetch(1); }
|
clearFilters() { this.search = ''; this.fetch(1); }
|
||||||
|
|
||||||
async onExport(): Promise<void> {
|
|
||||||
if (this.exporting) return;
|
|
||||||
this.exporting = true;
|
|
||||||
|
|
||||||
try {
|
|
||||||
const baseRows = await this.fetchAllRowsForExport();
|
|
||||||
const rows = await this.fetchDetailedRowsForExport(baseRows);
|
|
||||||
if (!rows.length) {
|
|
||||||
this.showToast('Nenhum registro encontrado para exportar.', 'danger');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const timestamp = this.tableExportService.buildTimestamp();
|
|
||||||
const fileName = `dados_usuarios_${this.tipoFilter.toLowerCase()}_${timestamp}`;
|
|
||||||
|
|
||||||
await this.tableExportService.exportAsXlsx<UserDataRow>({
|
|
||||||
fileName,
|
|
||||||
sheetName: 'DadosUsuarios',
|
|
||||||
rows,
|
|
||||||
columns: [
|
|
||||||
{ header: 'ID', value: (row) => row.id ?? '' },
|
|
||||||
{ header: 'Tipo', value: (row) => this.normalizeTipo(row) },
|
|
||||||
{ header: 'Cliente', value: (row) => row.cliente ?? '' },
|
|
||||||
{
|
|
||||||
header: this.tipoFilter === 'PJ' ? 'Razao Social' : 'Nome',
|
|
||||||
value: (row) => (this.normalizeTipo(row) === 'PJ' ? (row.razaoSocial ?? row.cliente ?? '') : (row.nome ?? row.cliente ?? '')),
|
|
||||||
},
|
|
||||||
{ header: 'Item', type: 'number', value: (row) => this.toNullableNumber(row.item) ?? 0 },
|
|
||||||
{ header: 'Linha', value: (row) => row.linha ?? '' },
|
|
||||||
{ header: 'CPF', value: (row) => row.cpf ?? '' },
|
|
||||||
{ header: 'CNPJ', value: (row) => row.cnpj ?? '' },
|
|
||||||
{ header: 'E-mail', value: (row) => row.email ?? '' },
|
|
||||||
{ header: 'Celular', value: (row) => row.celular ?? '' },
|
|
||||||
{ header: 'Telefone Fixo', value: (row) => row.telefoneFixo ?? '' },
|
|
||||||
{ header: 'RG', value: (row) => row.rg ?? '' },
|
|
||||||
{ header: 'Endereco', value: (row) => row.endereco ?? '' },
|
|
||||||
{ header: 'Data de Nascimento', type: 'date', value: (row) => row.dataNascimento ?? '' },
|
|
||||||
],
|
|
||||||
});
|
|
||||||
|
|
||||||
this.showToast(`Planilha exportada com ${rows.length} registro(s).`, 'success');
|
|
||||||
} catch {
|
|
||||||
this.showToast('Erro ao exportar planilha.', 'danger');
|
|
||||||
} finally {
|
|
||||||
this.exporting = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async fetchAllRowsForExport(): Promise<UserDataRow[]> {
|
|
||||||
const pageSize = 500;
|
|
||||||
let page = 1;
|
|
||||||
let expectedTotal = 0;
|
|
||||||
const all: UserDataRow[] = [];
|
|
||||||
|
|
||||||
while (page <= 500) {
|
|
||||||
const response = await firstValueFrom(
|
|
||||||
this.service.getRows({
|
|
||||||
search: this.search?.trim(),
|
|
||||||
tipo: this.tipoFilter,
|
|
||||||
page,
|
|
||||||
pageSize,
|
|
||||||
sortBy: 'item',
|
|
||||||
sortDir: 'asc',
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
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.sort((a, b) => {
|
|
||||||
const byClient = (a.cliente ?? '').localeCompare(b.cliente ?? '', 'pt-BR', { sensitivity: 'base' });
|
|
||||||
if (byClient !== 0) return byClient;
|
|
||||||
return (this.toNullableNumber(a.item) ?? 0) - (this.toNullableNumber(b.item) ?? 0);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private async fetchDetailedRowsForExport(rows: UserDataRow[]): Promise<UserDataRow[]> {
|
|
||||||
if (!rows.length) return [];
|
|
||||||
|
|
||||||
const detailed: UserDataRow[] = [];
|
|
||||||
const chunkSize = 10;
|
|
||||||
|
|
||||||
for (let i = 0; i < rows.length; i += chunkSize) {
|
|
||||||
const chunk = rows.slice(i, i + chunkSize);
|
|
||||||
const resolved = await Promise.all(
|
|
||||||
chunk.map(async (row) => {
|
|
||||||
try {
|
|
||||||
return await firstValueFrom(this.service.getById(row.id));
|
|
||||||
} catch {
|
|
||||||
return row;
|
|
||||||
}
|
|
||||||
})
|
|
||||||
);
|
|
||||||
detailed.push(...resolved);
|
|
||||||
}
|
|
||||||
|
|
||||||
return detailed;
|
|
||||||
}
|
|
||||||
|
|
||||||
onPageSizeChange() {
|
onPageSizeChange() {
|
||||||
this.page = 1;
|
this.page = 1;
|
||||||
this.fetch();
|
this.fetch();
|
||||||
}
|
}
|
||||||
|
|
||||||
goToPage(p: number) {
|
goToPage(p: number) {
|
||||||
this.page = clampPage(p, this.totalPages);
|
this.page = p;
|
||||||
this.fetch();
|
this.fetch();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -29,53 +29,15 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="operadora-filter-row fade-in-up" [style.animation-delay]="'80ms'" *ngIf="!isCliente">
|
|
||||||
<div class="operadora-filter-label">
|
|
||||||
<i class="bi bi-funnel-fill"></i>
|
|
||||||
<span>Filtro de Operadora</span>
|
|
||||||
</div>
|
|
||||||
<div class="filter-tabs">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class="filter-tab"
|
|
||||||
*ngFor="let option of operadoraFilters; trackBy: trackByOperadoraFilter"
|
|
||||||
[class.active]="operadoraFilter === option.value"
|
|
||||||
[disabled]="operadoraFilterLoading"
|
|
||||||
(click)="onOperadoraFilterChange(option.value)">
|
|
||||||
{{ option.label }}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="hero-grid fade-in-up" [style.animation-delay]="'100ms'" *ngIf="!isCliente || clientOverview.hasData">
|
<div class="hero-grid fade-in-up" [style.animation-delay]="'100ms'" *ngIf="!isCliente || clientOverview.hasData">
|
||||||
<div
|
<div class="hero-card" *ngFor="let k of kpis; trackBy: trackByKpiKey">
|
||||||
class="hero-card"
|
|
||||||
*ngFor="let k of kpis; trackBy: trackByKpiKey"
|
|
||||||
[class.hero-card-clickable]="isKpiClickable(k)"
|
|
||||||
[attr.role]="isKpiClickable(k) ? 'button' : null"
|
|
||||||
[attr.tabindex]="isKpiClickable(k) ? 0 : null"
|
|
||||||
(click)="onKpiClick(k)"
|
|
||||||
(keydown)="onKpiCardKeydown($event, k)">
|
|
||||||
<div class="hero-icon">
|
<div class="hero-icon">
|
||||||
<i [class]="k.icon"></i>
|
<i [class]="k.icon"></i>
|
||||||
</div>
|
</div>
|
||||||
<div class="hero-data">
|
<div class="hero-data">
|
||||||
<span class="hero-label">{{ k.title }}</span>
|
<span class="hero-label">{{ k.title }}</span>
|
||||||
<div class="hero-value-row">
|
<span class="hero-value">{{ k.value }}</span>
|
||||||
<span class="hero-value">{{ k.value }}</span>
|
<span class="hero-hint" *ngIf="k.hint">{{ k.hint }}</span>
|
||||||
<span
|
|
||||||
class="hero-trend"
|
|
||||||
[ngClass]="{
|
|
||||||
'trend-up': k.trend === 'up',
|
|
||||||
'trend-down': k.trend === 'down',
|
|
||||||
'trend-stable': k.trend === 'stable'
|
|
||||||
}"
|
|
||||||
[attr.aria-label]="k.trend === 'up' ? 'Aumento nas últimas 24 horas' : k.trend === 'down' ? 'Queda nas últimas 24 horas' : 'Sem alteração nas últimas 24 horas'">
|
|
||||||
<i *ngIf="k.trend === 'up'" class="bi bi-arrow-up"></i>
|
|
||||||
<i *ngIf="k.trend === 'down'" class="bi bi-arrow-down"></i>
|
|
||||||
<span *ngIf="k.trend === 'stable'">-</span>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -97,12 +59,7 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body-split">
|
<div class="card-body-split">
|
||||||
<div
|
<div class="chart-wrapper-pie">
|
||||||
class="chart-wrapper-pie chart-click-target"
|
|
||||||
role="button"
|
|
||||||
tabindex="0"
|
|
||||||
(click)="openChartModal('status')"
|
|
||||||
(keydown)="onChartTargetKeydown($event, 'status')">
|
|
||||||
<canvas #chartStatusPie></canvas>
|
<canvas #chartStatusPie></canvas>
|
||||||
</div>
|
</div>
|
||||||
<div class="status-list">
|
<div class="status-list">
|
||||||
|
|
@ -143,12 +100,7 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body-adicionais">
|
<div class="card-body-adicionais">
|
||||||
<div
|
<div class="chart-wrapper-pie-sm">
|
||||||
class="chart-wrapper-pie-sm chart-click-target"
|
|
||||||
role="button"
|
|
||||||
tabindex="0"
|
|
||||||
(click)="openChartModal('adicionaisComparativo')"
|
|
||||||
(keydown)="onChartTargetKeydown($event, 'adicionaisComparativo')">
|
|
||||||
<canvas #chartAdicionaisComparativo></canvas>
|
<canvas #chartAdicionaisComparativo></canvas>
|
||||||
</div>
|
</div>
|
||||||
<div class="compare-list">
|
<div class="compare-list">
|
||||||
|
|
@ -184,12 +136,7 @@
|
||||||
<p>Status de vencimento atual</p>
|
<p>Status de vencimento atual</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div class="chart-wrapper-pie">
|
||||||
class="chart-wrapper-pie chart-click-target"
|
|
||||||
role="button"
|
|
||||||
tabindex="0"
|
|
||||||
(click)="openChartModal('vigenciaBuckets')"
|
|
||||||
(keydown)="onChartTargetKeydown($event, 'vigenciaBuckets')">
|
|
||||||
<canvas #chartVigenciaSupervisao></canvas>
|
<canvas #chartVigenciaSupervisao></canvas>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -202,12 +149,7 @@
|
||||||
<p>Linhas com e sem serviço ativo</p>
|
<p>Linhas com e sem serviço ativo</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div class="chart-wrapper-pie">
|
||||||
class="chart-wrapper-pie chart-click-target"
|
|
||||||
role="button"
|
|
||||||
tabindex="0"
|
|
||||||
(click)="openChartModal('travel')"
|
|
||||||
(keydown)="onChartTargetKeydown($event, 'travel')">
|
|
||||||
<canvas #chartTravelMundo></canvas>
|
<canvas #chartTravelMundo></canvas>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -221,12 +163,7 @@
|
||||||
<p>Distribuição da base por faixa de franquia</p>
|
<p>Distribuição da base por faixa de franquia</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div class="chart-wrapper-bar compact">
|
||||||
class="chart-wrapper-bar compact chart-click-target"
|
|
||||||
role="button"
|
|
||||||
tabindex="0"
|
|
||||||
(click)="openChartModal('linhasFranquia')"
|
|
||||||
(keydown)="onChartTargetKeydown($event, 'linhasFranquia')">
|
|
||||||
<canvas #chartLinhasPorFranquia></canvas>
|
<canvas #chartLinhasPorFranquia></canvas>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -238,12 +175,7 @@
|
||||||
<p>Quantidade de linhas por serviço adicional ativo</p>
|
<p>Quantidade de linhas por serviço adicional ativo</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div class="chart-wrapper-bar compact">
|
||||||
class="chart-wrapper-bar compact chart-click-target"
|
|
||||||
role="button"
|
|
||||||
tabindex="0"
|
|
||||||
(click)="openChartModal('adicionaisPagos')"
|
|
||||||
(keydown)="onChartTargetKeydown($event, 'adicionaisPagos')">
|
|
||||||
<canvas #chartAdicionaisPagos></canvas>
|
<canvas #chartAdicionaisPagos></canvas>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -255,65 +187,13 @@
|
||||||
<p>Quantidade de linhas e-SIM e SIMCARD</p>
|
<p>Quantidade de linhas e-SIM e SIMCARD</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div class="chart-wrapper-pie">
|
||||||
class="chart-wrapper-pie chart-click-target"
|
|
||||||
role="button"
|
|
||||||
tabindex="0"
|
|
||||||
(click)="openChartModal('tipoChip')"
|
|
||||||
(keydown)="onChartTargetKeydown($event, 'tipoChip')">
|
|
||||||
<canvas #chartTipoChip></canvas>
|
<canvas #chartTipoChip></canvas>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="dashboard-section fade-in-up" [style.animation-delay]="'300ms'" *ngIf="showVivoComparison">
|
|
||||||
<div class="context-title">
|
|
||||||
<h2>Comparativo VIVO</h2>
|
|
||||||
<p>Comparação entre contas da operadora VIVO: MACROPHONY x LINE MÓVEL.</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="vivo-comparison-grid">
|
|
||||||
<div class="card-modern">
|
|
||||||
<div class="card-header-clean">
|
|
||||||
<div class="header-text">
|
|
||||||
<h3>Linhas por Empresa</h3>
|
|
||||||
<p>Volume total de linhas VIVO por empresa.</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
class="chart-wrapper-bar compact-half chart-click-target"
|
|
||||||
role="button"
|
|
||||||
tabindex="0"
|
|
||||||
(click)="openChartModal('vivoEmpresasLinhas')"
|
|
||||||
(keydown)="onChartTargetKeydown($event, 'vivoEmpresasLinhas')">
|
|
||||||
<canvas #chartVivoEmpresasLinhas></canvas>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="card-modern">
|
|
||||||
<div class="card-header-clean">
|
|
||||||
<div class="header-text">
|
|
||||||
<h3>Adicionais por Empresa</h3>
|
|
||||||
<p>Comparação de linhas com e sem adicionais pagos.</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
class="chart-wrapper-bar compact-half chart-click-target"
|
|
||||||
role="button"
|
|
||||||
tabindex="0"
|
|
||||||
(click)="openChartModal('vivoEmpresasAdicionais')"
|
|
||||||
(keydown)="onChartTargetKeydown($event, 'vivoEmpresasAdicionais')">
|
|
||||||
<canvas #chartVivoEmpresasAdicionais></canvas>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="vivo-comparison-empty" *ngIf="vivoComparison.macrophonyLinhas === 0 && vivoComparison.lineMovelLinhas === 0">
|
|
||||||
Não há linhas VIVO vinculadas às empresas MACROPHONY ou LINE MÓVEL para o filtro atual.
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="context-title fade-in-up" [style.animation-delay]="'320ms'">
|
<div class="context-title fade-in-up" [style.animation-delay]="'320ms'">
|
||||||
<h2>Página Resumo</h2>
|
<h2>Página Resumo</h2>
|
||||||
<p>Indicadores do Resumo focados em quantidade e distribuição de linhas.</p>
|
<p>Indicadores do Resumo focados em quantidade e distribuição de linhas.</p>
|
||||||
|
|
@ -360,50 +240,22 @@
|
||||||
<div class="analytics-grid" *ngIf="!resumoLoading && !resumoError">
|
<div class="analytics-grid" *ngIf="!resumoLoading && !resumoError">
|
||||||
<div class="mini-chart-card">
|
<div class="mini-chart-card">
|
||||||
<h6>Top Clientes (Qtd. Linhas)</h6>
|
<h6>Top Clientes (Qtd. Linhas)</h6>
|
||||||
<div
|
<div class="chart-area"><canvas #chartResumoTopClientes></canvas></div>
|
||||||
class="chart-area chart-click-target"
|
|
||||||
role="button"
|
|
||||||
tabindex="0"
|
|
||||||
(click)="openChartModal('resumoTopClientes')"
|
|
||||||
(keydown)="onChartTargetKeydown($event, 'resumoTopClientes')">
|
|
||||||
<canvas #chartResumoTopClientes></canvas>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mini-chart-card">
|
<div class="mini-chart-card">
|
||||||
<h6>Top Planos (Qtd. Linhas)</h6>
|
<h6>Top Planos (Qtd. Linhas)</h6>
|
||||||
<div
|
<div class="chart-area"><canvas #chartResumoTopPlanos></canvas></div>
|
||||||
class="chart-area chart-click-target"
|
|
||||||
role="button"
|
|
||||||
tabindex="0"
|
|
||||||
(click)="openChartModal('resumoTopPlanos')"
|
|
||||||
(keydown)="onChartTargetKeydown($event, 'resumoTopPlanos')">
|
|
||||||
<canvas #chartResumoTopPlanos></canvas>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mini-chart-card">
|
<div class="mini-chart-card">
|
||||||
<h6>PF vs PJ (Qtd. Linhas)</h6>
|
<h6>PF vs PJ (Qtd. Linhas)</h6>
|
||||||
<div
|
<div class="chart-area"><canvas #chartResumoPfPjLinhas></canvas></div>
|
||||||
class="chart-area chart-click-target"
|
|
||||||
role="button"
|
|
||||||
tabindex="0"
|
|
||||||
(click)="openChartModal('resumoPfPj')"
|
|
||||||
(keydown)="onChartTargetKeydown($event, 'resumoPfPj')">
|
|
||||||
<canvas #chartResumoPfPjLinhas></canvas>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mini-chart-card">
|
<div class="mini-chart-card">
|
||||||
<h6>Reserva por DDD</h6>
|
<h6>Reserva por DDD</h6>
|
||||||
<div
|
<div class="chart-area"><canvas #chartResumoReservaDdd></canvas></div>
|
||||||
class="chart-area chart-click-target"
|
|
||||||
role="button"
|
|
||||||
tabindex="0"
|
|
||||||
(click)="openChartModal('resumoReservaDdd')"
|
|
||||||
(keydown)="onChartTargetKeydown($event, 'resumoReservaDdd')">
|
|
||||||
<canvas #chartResumoReservaDdd></canvas>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mini-chart-card mini-metric-card">
|
<div class="mini-chart-card mini-metric-card">
|
||||||
|
|
@ -442,12 +294,7 @@
|
||||||
<p>Histórico mensal de mudanças de plano/aparelho</p>
|
<p>Histórico mensal de mudanças de plano/aparelho</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div class="chart-wrapper-bar compact-half">
|
||||||
class="chart-wrapper-bar compact-half chart-click-target"
|
|
||||||
role="button"
|
|
||||||
tabindex="0"
|
|
||||||
(click)="openChartModal('mureg12')"
|
|
||||||
(keydown)="onChartTargetKeydown($event, 'mureg12')">
|
|
||||||
<canvas #chartMureg12></canvas>
|
<canvas #chartMureg12></canvas>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -459,12 +306,7 @@
|
||||||
<p>Histórico mensal de trocas realizadas</p>
|
<p>Histórico mensal de trocas realizadas</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div class="chart-wrapper-bar compact-half">
|
||||||
class="chart-wrapper-bar compact-half chart-click-target"
|
|
||||||
role="button"
|
|
||||||
tabindex="0"
|
|
||||||
(click)="openChartModal('troca12')"
|
|
||||||
(keydown)="onChartTargetKeydown($event, 'troca12')">
|
|
||||||
<canvas #chartTroca12></canvas>
|
<canvas #chartTroca12></canvas>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -477,12 +319,7 @@
|
||||||
<p>Contratos a encerrar por mês</p>
|
<p>Contratos a encerrar por mês</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div class="chart-wrapper-bar compact-half">
|
||||||
class="chart-wrapper-bar compact-half chart-click-target"
|
|
||||||
role="button"
|
|
||||||
tabindex="0"
|
|
||||||
(click)="openChartModal('vigenciaMesAno')"
|
|
||||||
(keydown)="onChartTargetKeydown($event, 'vigenciaMesAno')">
|
|
||||||
<canvas #chartVigenciaMesAno></canvas>
|
<canvas #chartVigenciaMesAno></canvas>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -508,12 +345,7 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body-split">
|
<div class="card-body-split">
|
||||||
<div
|
<div class="chart-wrapper-pie">
|
||||||
class="chart-wrapper-pie chart-click-target"
|
|
||||||
role="button"
|
|
||||||
tabindex="0"
|
|
||||||
(click)="openChartModal('status')"
|
|
||||||
(keydown)="onChartTargetKeydown($event, 'status')">
|
|
||||||
<canvas #chartStatusPie></canvas>
|
<canvas #chartStatusPie></canvas>
|
||||||
</div>
|
</div>
|
||||||
<div class="status-list">
|
<div class="status-list">
|
||||||
|
|
@ -534,12 +366,7 @@
|
||||||
<p>Quantidade de linhas por faixa de franquia contratada</p>
|
<p>Quantidade de linhas por faixa de franquia contratada</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div class="chart-wrapper-bar compact-half">
|
||||||
class="chart-wrapper-bar compact-half chart-click-target"
|
|
||||||
role="button"
|
|
||||||
tabindex="0"
|
|
||||||
(click)="openChartModal('linhasFranquia')"
|
|
||||||
(keydown)="onChartTargetKeydown($event, 'linhasFranquia')">
|
|
||||||
<canvas #chartLinhasPorFranquia></canvas>
|
<canvas #chartLinhasPorFranquia></canvas>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -554,12 +381,7 @@
|
||||||
<p>Planos com maior volume na sua operação</p>
|
<p>Planos com maior volume na sua operação</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div class="chart-wrapper-bar compact">
|
||||||
class="chart-wrapper-bar compact chart-click-target"
|
|
||||||
role="button"
|
|
||||||
tabindex="0"
|
|
||||||
(click)="openChartModal('resumoTopPlanos')"
|
|
||||||
(keydown)="onChartTargetKeydown($event, 'resumoTopPlanos')">
|
|
||||||
<canvas #chartResumoTopPlanos></canvas>
|
<canvas #chartResumoTopPlanos></canvas>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -572,12 +394,7 @@
|
||||||
<p>Apenas usuários de fato (sem bloqueados/aguardando)</p>
|
<p>Apenas usuários de fato (sem bloqueados/aguardando)</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div class="chart-wrapper-bar compact">
|
||||||
class="chart-wrapper-bar compact chart-click-target"
|
|
||||||
role="button"
|
|
||||||
tabindex="0"
|
|
||||||
(click)="openChartModal('resumoTopClientes')"
|
|
||||||
(keydown)="onChartTargetKeydown($event, 'resumoTopClientes')">
|
|
||||||
<canvas #chartResumoTopClientes></canvas>
|
<canvas #chartResumoTopClientes></canvas>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -590,12 +407,7 @@
|
||||||
<p>Distribuição entre e-SIM, SIMCARD e outros</p>
|
<p>Distribuição entre e-SIM, SIMCARD e outros</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div class="chart-wrapper-pie">
|
||||||
class="chart-wrapper-pie chart-click-target"
|
|
||||||
role="button"
|
|
||||||
tabindex="0"
|
|
||||||
(click)="openChartModal('tipoChip')"
|
|
||||||
(keydown)="onChartTargetKeydown($event, 'tipoChip')">
|
|
||||||
<canvas #chartTipoChip></canvas>
|
<canvas #chartTipoChip></canvas>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -622,54 +434,5 @@
|
||||||
</div>
|
</div>
|
||||||
</ng-template>
|
</ng-template>
|
||||||
</ng-template>
|
</ng-template>
|
||||||
|
|
||||||
<div
|
|
||||||
class="chart-modal-overlay"
|
|
||||||
*ngIf="chartModalOpen"
|
|
||||||
role="dialog"
|
|
||||||
aria-modal="true"
|
|
||||||
[attr.aria-label]="chartModalTitle || 'Gráfico expandido'"
|
|
||||||
(click)="closeChartModal()">
|
|
||||||
<div class="chart-modal-card" (click)="$event.stopPropagation()">
|
|
||||||
<div class="chart-modal-header">
|
|
||||||
<div class="chart-modal-title-wrap">
|
|
||||||
<h3>{{ chartModalTitle }}</h3>
|
|
||||||
<p *ngIf="chartModalSubtitle">{{ chartModalSubtitle }}</p>
|
|
||||||
</div>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class="chart-modal-close"
|
|
||||||
aria-label="Fechar gráfico expandido"
|
|
||||||
(click)="closeChartModal()">
|
|
||||||
<i class="bi bi-x-lg"></i>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<div class="chart-modal-body">
|
|
||||||
<div class="chart-modal-content">
|
|
||||||
<div class="chart-modal-visual">
|
|
||||||
<canvas #chartExpandedCanvas></canvas>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
class="chart-modal-info"
|
|
||||||
*ngIf="chartModalInfoRows.length > 0"
|
|
||||||
[style.--dataset-cols]="chartModalDatasetHeaders.length">
|
|
||||||
<div class="chart-modal-info-head">
|
|
||||||
<span class="col-label">Categoria</span>
|
|
||||||
<span class="col-value" *ngFor="let dataset of chartModalDatasetHeaders">{{ dataset }}</span>
|
|
||||||
</div>
|
|
||||||
<div class="chart-modal-info-row" *ngFor="let row of chartModalInfoRows">
|
|
||||||
<span class="col-label">{{ row.label }}</span>
|
|
||||||
<span class="col-value" *ngFor="let cell of row.cells">{{ cell.valueText }}</span>
|
|
||||||
</div>
|
|
||||||
<div class="chart-modal-info-total" *ngIf="chartModalDatasetTotals.length > 0">
|
|
||||||
<span *ngFor="let total of chartModalDatasetTotals">
|
|
||||||
Total {{ total.dataset }}: <strong>{{ total.totalText }}</strong>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
|
||||||
|
|
@ -98,72 +98,6 @@
|
||||||
@media(max-width: 768px) { flex-direction: column; align-items: flex-start; gap: 16px; }
|
@media(max-width: 768px) { flex-direction: column; align-items: flex-start; gap: 16px; }
|
||||||
}
|
}
|
||||||
|
|
||||||
.operadora-filter-row {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
gap: 16px;
|
|
||||||
margin: -8px 0 22px;
|
|
||||||
padding: 14px 16px;
|
|
||||||
border-radius: 14px;
|
|
||||||
background: rgba(255, 255, 255, 0.88);
|
|
||||||
border: 1px solid rgba(15, 23, 42, 0.08);
|
|
||||||
box-shadow: var(--shadow-sm);
|
|
||||||
|
|
||||||
@media (max-width: 840px) {
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: stretch;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.operadora-filter-label {
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 8px;
|
|
||||||
font-size: 12px;
|
|
||||||
color: var(--text-muted);
|
|
||||||
font-weight: 700;
|
|
||||||
letter-spacing: 0.02em;
|
|
||||||
|
|
||||||
i {
|
|
||||||
color: var(--brand);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.filter-tabs {
|
|
||||||
display: inline-flex;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
gap: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.filter-tab {
|
|
||||||
border: 1px solid rgba(15, 23, 42, 0.15);
|
|
||||||
background: #fff;
|
|
||||||
color: var(--text-muted);
|
|
||||||
border-radius: 999px;
|
|
||||||
padding: 6px 12px;
|
|
||||||
font-size: 12px;
|
|
||||||
font-weight: 700;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: all 0.2s ease;
|
|
||||||
|
|
||||||
&:hover:not(:disabled) {
|
|
||||||
border-color: rgba(227, 61, 207, 0.45);
|
|
||||||
color: var(--brand);
|
|
||||||
}
|
|
||||||
|
|
||||||
&.active {
|
|
||||||
background: var(--brand-soft);
|
|
||||||
border-color: rgba(227, 61, 207, 0.45);
|
|
||||||
color: var(--brand);
|
|
||||||
}
|
|
||||||
|
|
||||||
&:disabled {
|
|
||||||
opacity: 0.55;
|
|
||||||
cursor: not-allowed;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.badge-pill {
|
.badge-pill {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|
@ -244,7 +178,6 @@
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 12px;
|
gap: 12px;
|
||||||
cursor: default;
|
|
||||||
transition: all 0.2s ease;
|
transition: all 0.2s ease;
|
||||||
box-shadow: var(--shadow-sm);
|
box-shadow: var(--shadow-sm);
|
||||||
|
|
||||||
|
|
@ -256,15 +189,6 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.hero-card.hero-card-clickable {
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.hero-card.hero-card-clickable:focus-visible {
|
|
||||||
outline: 2px solid rgba(227, 61, 207, 0.7);
|
|
||||||
outline-offset: 2px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.hero-icon {
|
.hero-icon {
|
||||||
width: 40px;
|
width: 40px;
|
||||||
height: 40px;
|
height: 40px;
|
||||||
|
|
@ -289,47 +213,11 @@
|
||||||
letter-spacing: 0.02em;
|
letter-spacing: 0.02em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.hero-value-row {
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 10px;
|
|
||||||
margin-top: 2px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.hero-value {
|
.hero-value {
|
||||||
font-size: 24px;
|
font-size: 24px;
|
||||||
font-weight: 800;
|
font-weight: 800;
|
||||||
color: var(--text-main);
|
color: var(--text-main);
|
||||||
line-height: 1;
|
margin-top: 2px;
|
||||||
}
|
|
||||||
|
|
||||||
.hero-trend {
|
|
||||||
min-width: 22px;
|
|
||||||
height: 22px;
|
|
||||||
border-radius: 999px;
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
font-size: 12px;
|
|
||||||
font-weight: 800;
|
|
||||||
border: 1px solid rgba(15, 23, 42, 0.08);
|
|
||||||
background: rgba(148, 163, 184, 0.12);
|
|
||||||
|
|
||||||
&.trend-up {
|
|
||||||
color: #15803d;
|
|
||||||
background: rgba(34, 197, 94, 0.16);
|
|
||||||
border-color: rgba(34, 197, 94, 0.22);
|
|
||||||
}
|
|
||||||
|
|
||||||
&.trend-down {
|
|
||||||
color: #b91c1c;
|
|
||||||
background: rgba(239, 68, 68, 0.16);
|
|
||||||
border-color: rgba(239, 68, 68, 0.22);
|
|
||||||
}
|
|
||||||
|
|
||||||
&.trend-stable {
|
|
||||||
color: #64748b;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.hero-hint {
|
.hero-hint {
|
||||||
|
|
@ -494,22 +382,6 @@
|
||||||
&.compact-half { height: 200px; }
|
&.compact-half { height: 200px; }
|
||||||
}
|
}
|
||||||
|
|
||||||
.chart-click-target {
|
|
||||||
cursor: zoom-in;
|
|
||||||
border-radius: 12px;
|
|
||||||
transition: transform 0.2s ease, box-shadow 0.2s ease;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
transform: translateY(-1px);
|
|
||||||
box-shadow: 0 10px 18px -16px rgba(17, 18, 20, 0.65);
|
|
||||||
}
|
|
||||||
|
|
||||||
&:focus-visible {
|
|
||||||
outline: 2px solid rgba(3, 15, 170, 0.26);
|
|
||||||
outline-offset: 2px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.card-adicionais .card-body-adicionais {
|
.card-adicionais .card-body-adicionais {
|
||||||
padding: 14px 16px 12px;
|
padding: 14px 16px 12px;
|
||||||
display: grid;
|
display: grid;
|
||||||
|
|
@ -748,221 +620,7 @@
|
||||||
@media(max-width: 1080px) { grid-template-columns: 1fr; }
|
@media(max-width: 1080px) { grid-template-columns: 1fr; }
|
||||||
}
|
}
|
||||||
|
|
||||||
.vivo-comparison-grid {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
||||||
gap: 16px;
|
|
||||||
|
|
||||||
@media (max-width: 980px) {
|
|
||||||
grid-template-columns: 1fr;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.vivo-comparison-empty {
|
|
||||||
margin-top: 10px;
|
|
||||||
padding: 12px 14px;
|
|
||||||
border-radius: 10px;
|
|
||||||
background: rgba(148, 163, 184, 0.12);
|
|
||||||
border: 1px solid rgba(148, 163, 184, 0.28);
|
|
||||||
color: var(--text-muted);
|
|
||||||
font-size: 12px;
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
|
|
||||||
.chart-modal-overlay {
|
|
||||||
position: fixed;
|
|
||||||
inset: 0;
|
|
||||||
z-index: 1200;
|
|
||||||
background: rgba(10, 14, 35, 0.58);
|
|
||||||
backdrop-filter: blur(5px);
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
padding: 28px 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.chart-modal-card {
|
|
||||||
width: min(1120px, 96vw);
|
|
||||||
max-height: min(86vh, 860px);
|
|
||||||
background: #fff;
|
|
||||||
border: 1px solid rgba(15, 23, 42, 0.08);
|
|
||||||
border-radius: 18px;
|
|
||||||
box-shadow: 0 30px 70px -26px rgba(2, 8, 23, 0.65);
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
overflow: hidden;
|
|
||||||
animation: modalChartIn 0.22s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.chart-modal-header {
|
|
||||||
padding: 14px 18px;
|
|
||||||
border-bottom: 1px solid rgba(15, 23, 42, 0.08);
|
|
||||||
display: flex;
|
|
||||||
align-items: flex-start;
|
|
||||||
justify-content: space-between;
|
|
||||||
gap: 14px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.chart-modal-title-wrap {
|
|
||||||
h3 {
|
|
||||||
margin: 0;
|
|
||||||
font-size: 16px;
|
|
||||||
font-weight: 800;
|
|
||||||
color: var(--text-main);
|
|
||||||
}
|
|
||||||
|
|
||||||
p {
|
|
||||||
margin: 4px 0 0;
|
|
||||||
font-size: 12px;
|
|
||||||
color: var(--text-muted);
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.chart-modal-close {
|
|
||||||
width: 34px;
|
|
||||||
height: 34px;
|
|
||||||
border-radius: 10px;
|
|
||||||
border: 1px solid rgba(15, 23, 42, 0.12);
|
|
||||||
background: #fff;
|
|
||||||
color: rgba(17, 18, 20, 0.7);
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: all 0.2s ease;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
border-color: rgba(227, 61, 207, 0.35);
|
|
||||||
color: var(--brand);
|
|
||||||
background: var(--brand-soft);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.chart-modal-body {
|
|
||||||
position: relative;
|
|
||||||
height: min(72vh, 680px);
|
|
||||||
min-height: 360px;
|
|
||||||
padding: 14px 16px 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.chart-modal-content {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: minmax(0, 1.25fr) minmax(300px, 0.75fr);
|
|
||||||
gap: 14px;
|
|
||||||
height: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.chart-modal-visual {
|
|
||||||
position: relative;
|
|
||||||
min-height: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.chart-modal-info {
|
|
||||||
border: 1px solid rgba(15, 23, 42, 0.08);
|
|
||||||
border-radius: 12px;
|
|
||||||
background: #f8fafc;
|
|
||||||
overflow: auto;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
|
|
||||||
.chart-modal-info-head,
|
|
||||||
.chart-modal-info-row {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: minmax(120px, 1.2fr) repeat(var(--dataset-cols, 1), minmax(78px, 1fr));
|
|
||||||
gap: 10px;
|
|
||||||
padding: 10px 12px;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.chart-modal-info-head {
|
|
||||||
position: sticky;
|
|
||||||
top: 0;
|
|
||||||
z-index: 1;
|
|
||||||
background: #eef2ff;
|
|
||||||
border-bottom: 1px solid rgba(15, 23, 42, 0.08);
|
|
||||||
font-size: 11px;
|
|
||||||
font-weight: 800;
|
|
||||||
color: #334155;
|
|
||||||
text-transform: uppercase;
|
|
||||||
letter-spacing: 0.03em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.chart-modal-info-row {
|
|
||||||
border-bottom: 1px solid rgba(15, 23, 42, 0.06);
|
|
||||||
font-size: 12px;
|
|
||||||
color: #0f172a;
|
|
||||||
font-weight: 600;
|
|
||||||
background: #fff;
|
|
||||||
|
|
||||||
&:nth-child(odd) {
|
|
||||||
background: #fdfdff;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.chart-modal-info .col-label {
|
|
||||||
text-align: left;
|
|
||||||
word-break: break-word;
|
|
||||||
}
|
|
||||||
|
|
||||||
.chart-modal-info .col-value {
|
|
||||||
text-align: right;
|
|
||||||
font-variant-numeric: tabular-nums;
|
|
||||||
}
|
|
||||||
|
|
||||||
.chart-modal-info-total {
|
|
||||||
display: flex;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
gap: 10px 16px;
|
|
||||||
border-top: 1px solid rgba(15, 23, 42, 0.08);
|
|
||||||
padding: 10px 12px 12px;
|
|
||||||
background: #f1f5f9;
|
|
||||||
font-size: 11px;
|
|
||||||
font-weight: 700;
|
|
||||||
color: #334155;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes modalChartIn {
|
|
||||||
from {
|
|
||||||
opacity: 0;
|
|
||||||
transform: translateY(8px) scale(0.985);
|
|
||||||
}
|
|
||||||
|
|
||||||
to {
|
|
||||||
opacity: 1;
|
|
||||||
transform: translateY(0) scale(1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Utils */
|
/* Utils */
|
||||||
.text-brand { color: var(--brand); }
|
.text-brand { color: var(--brand); }
|
||||||
.text-brand-dark { color: #b832a8; }
|
.text-brand-dark { color: #b832a8; }
|
||||||
.full-width { width: 100%; }
|
.full-width { width: 100%; }
|
||||||
|
|
||||||
@media (max-width: 760px) {
|
|
||||||
.chart-modal-overlay {
|
|
||||||
padding: 16px 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.chart-modal-card {
|
|
||||||
width: 100%;
|
|
||||||
border-radius: 14px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.chart-modal-body {
|
|
||||||
min-height: 300px;
|
|
||||||
height: min(72vh, 620px);
|
|
||||||
padding: 10px 10px 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.chart-modal-content {
|
|
||||||
grid-template-columns: 1fr;
|
|
||||||
grid-template-rows: minmax(200px, 1fr) minmax(140px, auto);
|
|
||||||
}
|
|
||||||
|
|
||||||
.chart-modal-info-head,
|
|
||||||
.chart-modal-info-row {
|
|
||||||
grid-template-columns: minmax(120px, 1.2fr) repeat(var(--dataset-cols, 1), minmax(72px, 1fr));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -33,12 +33,7 @@
|
||||||
<small class="subtitle">Totais, lucro e comparativo Vivo x Line</small>
|
<small class="subtitle">Totais, lucro e comparativo Vivo x Line</small>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="header-actions d-flex gap-2 justify-content-end" data-animate>
|
<div class="header-actions d-flex gap-2 justify-content-end" data-animate></div>
|
||||||
<button type="button" class="btn btn-glass btn-sm" (click)="onExport()" [disabled]="loading || exporting">
|
|
||||||
<span *ngIf="!exporting"><i class="bi bi-download me-1"></i> Exportar</span>
|
|
||||||
<span *ngIf="exporting"><span class="spinner-border spinner-border-sm me-2"></span> Exportando...</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- FILTROS -->
|
<!-- FILTROS -->
|
||||||
|
|
@ -189,6 +184,7 @@
|
||||||
|
|
||||||
<div class="select-wrapper">
|
<div class="select-wrapper">
|
||||||
<app-select class="select-glass" size="sm" [options]="pageSizeOptions" [(ngModel)]="pageSize" (ngModelChange)="onPageSizeChange()" [disabled]="loading"></app-select>
|
<app-select class="select-glass" size="sm" [options]="pageSizeOptions" [(ngModel)]="pageSize" (ngModelChange)="onPageSizeChange()" [disabled]="loading"></app-select>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -331,5 +327,248 @@
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<!-- MODAIS -->
|
<!-- MODAIS -->
|
||||||
|
<div class="modal-backdrop-custom" *ngIf="detailOpen || compareOpen || editOpen || deleteOpen" (click)="closeAllModals()"></div>
|
||||||
|
|
||||||
|
<div class="modal-custom" *ngIf="detailOpen || compareOpen || editOpen || deleteOpen" (click)="closeAllModals()">
|
||||||
|
|
||||||
|
<!-- DETAIL MODAL -->
|
||||||
|
<div *ngIf="detailOpen" #detailModal class="modal-card modal-xl-custom" (click)="$event.stopPropagation()">
|
||||||
|
<div class="modal-header">
|
||||||
|
<div class="modal-title">
|
||||||
|
<span class="icon-bg detail-icon"><i class="bi bi-receipt"></i></span>
|
||||||
|
Detalhes do Faturamento
|
||||||
|
</div>
|
||||||
|
<button class="btn btn-sm btn-icon" (click)="closeAllModals()"><i class="bi bi-x-lg"></i></button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="modal-body modern-body bg-light-gray" *ngIf="detailData; else detailLoading">
|
||||||
|
<div class="mb-3 d-flex flex-wrap align-items-center justify-content-between gap-2">
|
||||||
|
<div class="d-flex flex-column">
|
||||||
|
<div class="fw-black detail-client">
|
||||||
|
{{ detailData.cliente || '—' }}
|
||||||
|
</div>
|
||||||
|
<small class="text-muted fw-bold">
|
||||||
|
ITEM: {{ detailData.item }} • QTD LINHAS: {{ detailData.qtdLinhas ?? 0 }}
|
||||||
|
</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="details-dashboard details-single">
|
||||||
|
|
||||||
|
<!-- IDENTIFICAÇÃO -->
|
||||||
|
<div class="detail-box">
|
||||||
|
<div class="box-header justify-content-center">
|
||||||
|
<span><i class="bi bi-person-badge me-2"></i> Identificação</span>
|
||||||
|
</div>
|
||||||
|
<div class="box-body">
|
||||||
|
<div class="info-grid">
|
||||||
|
<div class="info-item span-2">
|
||||||
|
<span class="lbl">Cliente</span>
|
||||||
|
<span class="val text-dark" [title]="detailData.cliente || ''">{{ detailData.cliente || '—' }}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="info-item">
|
||||||
|
<span class="lbl">Tipo</span>
|
||||||
|
<span class="val">{{ detailData.tipo || '—' }}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="info-item">
|
||||||
|
<span class="lbl">Qtd Linhas</span>
|
||||||
|
<span class="val">{{ detailData.qtdLinhas ?? 0 }}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="info-item span-2">
|
||||||
|
<span class="lbl">Aparelho</span>
|
||||||
|
<span class="val" [title]="detailData.aparelho || ''">{{ detailData.aparelho || '—' }}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="info-item span-2">
|
||||||
|
<span class="lbl">Forma de Pagamento</span>
|
||||||
|
<span class="val" [title]="detailData.formaPagamento || ''">{{ detailData.formaPagamento || '—' }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<ng-template #detailLoading>
|
||||||
|
<div class="p-5 text-center text-muted">Carregando detalhes...</div>
|
||||||
|
</ng-template>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- COMPARATIVO MODAL -->
|
||||||
|
<div *ngIf="compareOpen" #compareModal class="modal-card modal-lg" (click)="$event.stopPropagation()">
|
||||||
|
<div class="modal-header">
|
||||||
|
<div class="modal-title">
|
||||||
|
<span class="icon-bg compare-icon"><i class="bi bi-columns-gap"></i></span> Comparativo Vivo x Line
|
||||||
|
</div>
|
||||||
|
<button class="btn btn-sm btn-icon" (click)="closeAllModals()"><i class="bi bi-x-lg"></i></button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="modal-body modern-body bg-light-gray" *ngIf="compareData; else compareLoading">
|
||||||
|
<div class="finance-dashboard">
|
||||||
|
<div class="finance-card vivo-card">
|
||||||
|
<div class="card-header-f"><i class="bi bi-telephone-fill me-2"></i> Vivo</div>
|
||||||
|
<div class="card-body-f">
|
||||||
|
<div class="row-item"><span>Franquia</span> <strong>{{ formatFranquia(compareData.franquiaVivo) }}</strong></div>
|
||||||
|
<div class="divider"></div>
|
||||||
|
<div class="row-item total"><span>Valor Vivo (R$)</span> <strong>{{ formatMoney(compareData.valorContratoVivo) }}</strong></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="finance-card line-card">
|
||||||
|
<div class="card-header-f"><i class="bi bi-hdd-network-fill me-2"></i> Line Móvel</div>
|
||||||
|
<div class="card-body-f">
|
||||||
|
<div class="row-item"><span>Franquia Line</span> <strong>{{ formatFranquia(compareData.franquiaLine) }}</strong></div>
|
||||||
|
<div class="divider"></div>
|
||||||
|
<div class="row-item total"><span>Valor Line (R$)</span> <strong>{{ formatMoney(compareData.valorContratoLine) }}</strong></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="finance-summary mt-3">
|
||||||
|
<div class="summary-item">
|
||||||
|
<span class="lbl">Forma de Pagamento</span>
|
||||||
|
<span class="val text-dark">{{ compareData.formaPagamento || '—' }}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="vertical-line"></div>
|
||||||
|
|
||||||
|
<div class="summary-item">
|
||||||
|
<span class="lbl">Lucro</span>
|
||||||
|
<span class="val text-brand">{{ formatMoney(compareData.lucro) }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<ng-template #compareLoading>
|
||||||
|
<div class="p-5 text-center text-muted">Carregando comparativo...</div>
|
||||||
|
</ng-template>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- EDIT MODAL -->
|
||||||
|
<div *ngIf="editOpen" class="modal-card modal-xl-custom" (click)="$event.stopPropagation()">
|
||||||
|
<div class="modal-header">
|
||||||
|
<div class="modal-title">
|
||||||
|
<span class="icon-bg primary-soft"><i class="bi bi-pencil-square"></i></span> Editar Faturamento
|
||||||
|
</div>
|
||||||
|
<button class="btn btn-sm btn-icon" (click)="closeEdit()"><i class="bi bi-x-lg"></i></button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="modal-body modern-body bg-light-gray" *ngIf="editModel">
|
||||||
|
<div class="edit-sections">
|
||||||
|
<details open class="detail-box">
|
||||||
|
<summary class="box-header">
|
||||||
|
<span><i class="bi bi-person-badge me-2"></i> Identificação</span>
|
||||||
|
<i class="bi bi-chevron-down ms-auto transition-icon"></i>
|
||||||
|
</summary>
|
||||||
|
<div class="box-body">
|
||||||
|
<div class="form-grid">
|
||||||
|
<div class="form-field span-2">
|
||||||
|
<label>Cliente</label>
|
||||||
|
<input class="form-control form-control-sm" [(ngModel)]="editModel.cliente" />
|
||||||
|
</div>
|
||||||
|
<div class="form-field">
|
||||||
|
<label>Tipo</label>
|
||||||
|
<select class="form-control form-control-sm" [(ngModel)]="editModel.tipo">
|
||||||
|
<option value="PF">PF</option>
|
||||||
|
<option value="PJ">PJ</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="form-field">
|
||||||
|
<label>Item</label>
|
||||||
|
<input class="form-control form-control-sm" type="number" [(ngModel)]="editModel.item" />
|
||||||
|
</div>
|
||||||
|
<div class="form-field">
|
||||||
|
<label>Qtd Linhas</label>
|
||||||
|
<input class="form-control form-control-sm" type="number" [(ngModel)]="editModel.qtdLinhas" />
|
||||||
|
</div>
|
||||||
|
<div class="form-field">
|
||||||
|
<label>Aparelho</label>
|
||||||
|
<input class="form-control form-control-sm" [(ngModel)]="editModel.aparelho" />
|
||||||
|
</div>
|
||||||
|
<div class="form-field span-2">
|
||||||
|
<label>Forma de Pagamento</label>
|
||||||
|
<input class="form-control form-control-sm" [(ngModel)]="editModel.formaPagamento" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</details>
|
||||||
|
|
||||||
|
<details open class="detail-box vivo-border">
|
||||||
|
<summary class="box-header header-vivo">
|
||||||
|
<span><i class="bi bi-telephone-fill me-2"></i> Faturamento Vivo</span>
|
||||||
|
<i class="bi bi-chevron-down ms-auto transition-icon"></i>
|
||||||
|
</summary>
|
||||||
|
<div class="box-body">
|
||||||
|
<div class="form-grid">
|
||||||
|
<div class="form-field">
|
||||||
|
<label>Franquia Vivo</label>
|
||||||
|
<input class="form-control form-control-sm" type="number" [(ngModel)]="editModel.franquiaVivo" />
|
||||||
|
</div>
|
||||||
|
<div class="form-field">
|
||||||
|
<label>Valor Vivo (R$)</label>
|
||||||
|
<input class="form-control form-control-sm" type="number" [(ngModel)]="editModel.valorContratoVivo" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</details>
|
||||||
|
|
||||||
|
<details open class="detail-box line-border">
|
||||||
|
<summary class="box-header header-line">
|
||||||
|
<span><i class="bi bi-hdd-network-fill me-2"></i> Faturamento Line</span>
|
||||||
|
<i class="bi bi-chevron-down ms-auto transition-icon"></i>
|
||||||
|
</summary>
|
||||||
|
<div class="box-body">
|
||||||
|
<div class="form-grid">
|
||||||
|
<div class="form-field">
|
||||||
|
<label>Franquia Line</label>
|
||||||
|
<input class="form-control form-control-sm" type="number" [(ngModel)]="editModel.franquiaLine" />
|
||||||
|
</div>
|
||||||
|
<div class="form-field">
|
||||||
|
<label>Valor Line (R$)</label>
|
||||||
|
<input class="form-control form-control-sm" type="number" [(ngModel)]="editModel.valorContratoLine" />
|
||||||
|
</div>
|
||||||
|
<div class="form-field span-2">
|
||||||
|
<label>Lucro (R$)</label>
|
||||||
|
<input class="form-control form-control-sm" type="number" [(ngModel)]="editModel.lucro" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</details>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="modal-footer p-3 text-end border-top bg-white">
|
||||||
|
<button class="btn btn-glass btn-sm me-2" (click)="closeEdit()">Cancelar</button>
|
||||||
|
<button class="btn btn-primary btn-sm" [disabled]="editSaving" (click)="saveEdit()">
|
||||||
|
{{ editSaving ? 'Salvando...' : 'Salvar' }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- DELETE MODAL -->
|
||||||
|
<div *ngIf="deleteOpen" class="modal-card modal-lg" (click)="$event.stopPropagation()">
|
||||||
|
<div class="modal-header">
|
||||||
|
<div class="modal-title">
|
||||||
|
<span class="icon-bg danger-soft"><i class="bi bi-trash"></i></span> Remover Faturamento
|
||||||
|
</div>
|
||||||
|
<button class="btn btn-sm btn-icon" (click)="cancelDelete()"><i class="bi bi-x-lg"></i></button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body modern-body bg-light-gray">
|
||||||
|
<div class="confirm-delete">
|
||||||
|
<div class="confirm-icon"><i class="bi bi-trash"></i></div>
|
||||||
|
<p class="mb-0">Confirma a exclusão do registro <strong>{{ deleteTarget?.cliente }}</strong>?</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer p-3 text-end border-top bg-white">
|
||||||
|
<button class="btn btn-glass btn-sm me-2" (click)="cancelDelete()">Cancelar</button>
|
||||||
|
<button class="btn btn-danger btn-sm" (click)="confirmDelete()">Excluir</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
<app-faturamento-modals [vm]="$any(vm)"></app-faturamento-modals>
|
|
||||||
|
|
|
||||||
|
|
@ -754,6 +754,24 @@
|
||||||
.fw-black { font-weight: 950; }
|
.fw-black { font-weight: 950; }
|
||||||
|
|
||||||
/* MODALS (mantidos do seu arquivo) */
|
/* MODALS (mantidos do seu arquivo) */
|
||||||
|
.modal-backdrop-custom {
|
||||||
|
position: fixed;
|
||||||
|
inset: 0;
|
||||||
|
background: rgba(0,0,0,0.45);
|
||||||
|
z-index: 9990;
|
||||||
|
backdrop-filter: blur(4px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-custom {
|
||||||
|
position: fixed;
|
||||||
|
inset: 0;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
z-index: 9995;
|
||||||
|
padding: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
.modal-card {
|
.modal-card {
|
||||||
background: #ffffff;
|
background: #ffffff;
|
||||||
border: 1px solid rgba(255,255,255,0.8);
|
border: 1px solid rgba(255,255,255,0.8);
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,6 @@ import { isPlatformBrowser, CommonModule } from '@angular/common';
|
||||||
import { FormsModule } from '@angular/forms';
|
import { FormsModule } from '@angular/forms';
|
||||||
import { HttpClientModule } from '@angular/common/http';
|
import { HttpClientModule } from '@angular/common/http';
|
||||||
import { CustomSelectComponent } from '../../components/custom-select/custom-select';
|
import { CustomSelectComponent } from '../../components/custom-select/custom-select';
|
||||||
import { FaturamentoModalsComponent } from '../../components/page-modals/faturamento-modals/faturamento-modals';
|
|
||||||
|
|
||||||
import {
|
import {
|
||||||
BillingService,
|
BillingService,
|
||||||
|
|
@ -26,17 +25,7 @@ import {
|
||||||
} from '../../services/billing';
|
} from '../../services/billing';
|
||||||
import { AuthService } from '../../services/auth.service';
|
import { AuthService } from '../../services/auth.service';
|
||||||
import { LinesService } from '../../services/lines.service';
|
import { LinesService } from '../../services/lines.service';
|
||||||
import { TableExportService } from '../../services/table-export.service';
|
|
||||||
import { confirmDeletionWithTyping } from '../../utils/destructive-confirmation';
|
import { confirmDeletionWithTyping } from '../../utils/destructive-confirmation';
|
||||||
import {
|
|
||||||
buildPageNumbers,
|
|
||||||
clampPage,
|
|
||||||
computePageEnd,
|
|
||||||
computePageStart,
|
|
||||||
computeTotalPages
|
|
||||||
} from '../../utils/pagination.util';
|
|
||||||
import { normalizeAccentInsensitive } from '../../utils/text-normalization.util';
|
|
||||||
import { firstValueFrom } from 'rxjs';
|
|
||||||
|
|
||||||
interface BillingClientGroup {
|
interface BillingClientGroup {
|
||||||
cliente: string;
|
cliente: string;
|
||||||
|
|
@ -49,12 +38,11 @@ interface BillingClientGroup {
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
standalone: true,
|
standalone: true,
|
||||||
imports: [CommonModule, FormsModule, HttpClientModule, CustomSelectComponent, FaturamentoModalsComponent],
|
imports: [CommonModule, FormsModule, HttpClientModule, CustomSelectComponent],
|
||||||
templateUrl: './faturamento.html',
|
templateUrl: './faturamento.html',
|
||||||
styleUrls: ['./faturamento.scss']
|
styleUrls: ['./faturamento.scss']
|
||||||
})
|
})
|
||||||
export class Faturamento implements AfterViewInit, OnDestroy {
|
export class Faturamento implements AfterViewInit, OnDestroy {
|
||||||
readonly vm = this;
|
|
||||||
toastMessage = '';
|
toastMessage = '';
|
||||||
@ViewChild('successToast', { static: false }) successToast!: ElementRef;
|
@ViewChild('successToast', { static: false }) successToast!: ElementRef;
|
||||||
|
|
||||||
|
|
@ -66,12 +54,10 @@ export class Faturamento implements AfterViewInit, OnDestroy {
|
||||||
private billing: BillingService,
|
private billing: BillingService,
|
||||||
private linesService: LinesService,
|
private linesService: LinesService,
|
||||||
private cdr: ChangeDetectorRef,
|
private cdr: ChangeDetectorRef,
|
||||||
private authService: AuthService,
|
private authService: AuthService
|
||||||
private tableExportService: TableExportService
|
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
loading = false;
|
loading = false;
|
||||||
exporting = false;
|
|
||||||
|
|
||||||
// filtros
|
// filtros
|
||||||
searchTerm = '';
|
searchTerm = '';
|
||||||
|
|
@ -232,6 +218,15 @@ export class Faturamento implements AfterViewInit, OnDestroy {
|
||||||
return s ? s : '—';
|
return s ? s : '—';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private normalizeText(s: any): string {
|
||||||
|
return (s ?? '')
|
||||||
|
.toString()
|
||||||
|
.trim()
|
||||||
|
.toUpperCase()
|
||||||
|
.normalize('NFD')
|
||||||
|
.replace(/[\u0300-\u036f]/g, '');
|
||||||
|
}
|
||||||
|
|
||||||
private buildGlobalSearchBlob(row: BillingItem): string {
|
private buildGlobalSearchBlob(row: BillingItem): string {
|
||||||
const parts = [
|
const parts = [
|
||||||
row.tipo,
|
row.tipo,
|
||||||
|
|
@ -247,13 +242,13 @@ export class Faturamento implements AfterViewInit, OnDestroy {
|
||||||
row.formaPagamento,
|
row.formaPagamento,
|
||||||
];
|
];
|
||||||
|
|
||||||
return normalizeAccentInsensitive(parts.join(' '));
|
return this.normalizeText(parts.join(' '));
|
||||||
}
|
}
|
||||||
|
|
||||||
private matchesTipo(itemTipo: any, filtro: TipoFiltro): boolean {
|
private matchesTipo(itemTipo: any, filtro: TipoFiltro): boolean {
|
||||||
if (filtro === 'ALL') return true;
|
if (filtro === 'ALL') return true;
|
||||||
|
|
||||||
const t = normalizeAccentInsensitive(itemTipo);
|
const t = this.normalizeText(itemTipo);
|
||||||
|
|
||||||
if (filtro === 'PF') return t === 'PF' || t.includes('FISICA');
|
if (filtro === 'PF') return t === 'PF' || t.includes('FISICA');
|
||||||
if (filtro === 'PJ') return t === 'PJ' || t.includes('JURIDICA');
|
if (filtro === 'PJ') return t === 'PJ' || t.includes('JURIDICA');
|
||||||
|
|
@ -420,85 +415,6 @@ export class Faturamento implements AfterViewInit, OnDestroy {
|
||||||
this.loadAllAndApply(forceReloadAll);
|
this.loadAllAndApply(forceReloadAll);
|
||||||
}
|
}
|
||||||
|
|
||||||
async onExport(): Promise<void> {
|
|
||||||
if (this.exporting) return;
|
|
||||||
this.exporting = true;
|
|
||||||
|
|
||||||
try {
|
|
||||||
const baseRows = this.getRowsForExport();
|
|
||||||
const rows = await this.fetchDetailedRowsForExport(baseRows);
|
|
||||||
if (!rows.length) {
|
|
||||||
await this.showToast('Nenhum registro encontrado para exportar.');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const timestamp = this.tableExportService.buildTimestamp();
|
|
||||||
const suffix = this.filterTipo === 'ALL' ? 'todos' : this.filterTipo.toLowerCase();
|
|
||||||
await this.tableExportService.exportAsXlsx<BillingItem>({
|
|
||||||
fileName: `faturamento_${suffix}_${timestamp}`,
|
|
||||||
sheetName: 'Faturamento',
|
|
||||||
rows,
|
|
||||||
columns: [
|
|
||||||
{ header: 'ID', value: (row) => row.id ?? '' },
|
|
||||||
{ header: 'Tipo', value: (row) => row.tipo ?? '' },
|
|
||||||
{ header: 'Item', type: 'number', value: (row) => this.toNullableNumber(row.item) ?? 0 },
|
|
||||||
{ header: 'Cliente', value: (row) => row.cliente ?? '' },
|
|
||||||
{ header: 'Qtd Linhas', type: 'number', value: (row) => this.toNullableNumber(row.qtdLinhas) ?? 0 },
|
|
||||||
{ header: 'Franquia Vivo', type: 'number', value: (row) => this.toNullableNumber(row.franquiaVivo) ?? 0 },
|
|
||||||
{ header: 'Valor Contrato Vivo', type: 'currency', value: (row) => this.toNullableNumber(row.valorContratoVivo) ?? 0 },
|
|
||||||
{ header: 'Franquia Line', type: 'number', value: (row) => this.toNullableNumber(row.franquiaLine) ?? 0 },
|
|
||||||
{ header: 'Valor Contrato Line', type: 'currency', value: (row) => this.toNullableNumber(row.valorContratoLine) ?? 0 },
|
|
||||||
{ header: 'Lucro', type: 'currency', value: (row) => this.toNullableNumber(row.lucro) ?? 0 },
|
|
||||||
{ header: 'Aparelho', value: (row) => row.aparelho ?? '' },
|
|
||||||
{ header: 'Forma de Pagamento', value: (row) => row.formaPagamento ?? '' },
|
|
||||||
{ header: 'Observacao', value: (row) => this.getObservacao(row) },
|
|
||||||
{ header: 'Criado Em', type: 'datetime', value: (row) => row.createdAt ?? '' },
|
|
||||||
{ header: 'Atualizado Em', type: 'datetime', value: (row) => row.updatedAt ?? '' },
|
|
||||||
],
|
|
||||||
});
|
|
||||||
|
|
||||||
await this.showToast(`Planilha exportada com ${rows.length} registro(s).`);
|
|
||||||
} catch {
|
|
||||||
await this.showToast('Erro ao exportar planilha.');
|
|
||||||
} finally {
|
|
||||||
this.exporting = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private getRowsForExport(): BillingItem[] {
|
|
||||||
const rows: BillingItem[] = [];
|
|
||||||
this.rowsByClient.forEach((items) => rows.push(...items));
|
|
||||||
|
|
||||||
return rows.sort((a, b) => {
|
|
||||||
const byClient = (a.cliente ?? '').localeCompare(b.cliente ?? '', 'pt-BR', { sensitivity: 'base' });
|
|
||||||
if (byClient !== 0) return byClient;
|
|
||||||
return (this.toNullableNumber(a.item) ?? 0) - (this.toNullableNumber(b.item) ?? 0);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private async fetchDetailedRowsForExport(rows: BillingItem[]): Promise<BillingItem[]> {
|
|
||||||
if (!rows.length) return [];
|
|
||||||
|
|
||||||
const detailed: BillingItem[] = [];
|
|
||||||
const chunkSize = 10;
|
|
||||||
|
|
||||||
for (let i = 0; i < rows.length; i += chunkSize) {
|
|
||||||
const chunk = rows.slice(i, i + chunkSize);
|
|
||||||
const resolved = await Promise.all(
|
|
||||||
chunk.map(async (row) => {
|
|
||||||
try {
|
|
||||||
return await firstValueFrom(this.billing.getById(row.id));
|
|
||||||
} catch {
|
|
||||||
return row;
|
|
||||||
}
|
|
||||||
})
|
|
||||||
);
|
|
||||||
detailed.push(...resolved);
|
|
||||||
}
|
|
||||||
|
|
||||||
return detailed;
|
|
||||||
}
|
|
||||||
|
|
||||||
private getAllItems(force = false): Promise<BillingItem[]> {
|
private getAllItems(force = false): Promise<BillingItem[]> {
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
|
|
||||||
|
|
@ -633,16 +549,16 @@ export class Faturamento implements AfterViewInit, OnDestroy {
|
||||||
let arr = [...baseTipo];
|
let arr = [...baseTipo];
|
||||||
|
|
||||||
if (this.selectedClients.length > 0) {
|
if (this.selectedClients.length > 0) {
|
||||||
const set = new Set(this.selectedClients.map((x) => normalizeAccentInsensitive(x)));
|
const set = new Set(this.selectedClients.map((x) => this.normalizeText(x)));
|
||||||
arr = arr.filter((r) => set.has(normalizeAccentInsensitive(r.cliente)));
|
arr = arr.filter((r) => set.has(this.normalizeText(r.cliente)));
|
||||||
}
|
}
|
||||||
|
|
||||||
const term = normalizeAccentInsensitive(this.searchTerm);
|
const term = this.normalizeText(this.searchTerm);
|
||||||
const resolvedClientsSet = new Set((this.searchResolvedClients ?? []).map((x) => normalizeAccentInsensitive(x)));
|
const resolvedClientsSet = new Set((this.searchResolvedClients ?? []).map((x) => this.normalizeText(x)));
|
||||||
if (term) {
|
if (term) {
|
||||||
arr = arr.filter((r) =>
|
arr = arr.filter((r) =>
|
||||||
this.buildGlobalSearchBlob(r).includes(term) ||
|
this.buildGlobalSearchBlob(r).includes(term) ||
|
||||||
(resolvedClientsSet.size > 0 && resolvedClientsSet.has(normalizeAccentInsensitive(r.cliente)))
|
(resolvedClientsSet.size > 0 && resolvedClientsSet.has(this.normalizeText(r.cliente)))
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -660,7 +576,7 @@ export class Faturamento implements AfterViewInit, OnDestroy {
|
||||||
|
|
||||||
totalLinhas += Number(r.qtdLinhas ?? 0) || 0;
|
totalLinhas += Number(r.qtdLinhas ?? 0) || 0;
|
||||||
|
|
||||||
const key = normalizeAccentInsensitive(c);
|
const key = this.normalizeText(c);
|
||||||
if (!key) continue;
|
if (!key) continue;
|
||||||
|
|
||||||
const vivo = Number(r.valorContratoVivo ?? 0) || 0;
|
const vivo = Number(r.valorContratoVivo ?? 0) || 0;
|
||||||
|
|
@ -736,7 +652,7 @@ export class Faturamento implements AfterViewInit, OnDestroy {
|
||||||
}
|
}
|
||||||
|
|
||||||
goToPage(p: number) {
|
goToPage(p: number) {
|
||||||
this.page = clampPage(p, this.totalPages);
|
this.page = Math.max(1, Math.min(this.totalPages, p));
|
||||||
this.applyGroupPagination();
|
this.applyGroupPagination();
|
||||||
this.cdr.detectChanges();
|
this.cdr.detectChanges();
|
||||||
}
|
}
|
||||||
|
|
@ -750,19 +666,27 @@ export class Faturamento implements AfterViewInit, OnDestroy {
|
||||||
}
|
}
|
||||||
|
|
||||||
get totalPages() {
|
get totalPages() {
|
||||||
return computeTotalPages(this.total || 0, this.pageSize);
|
return Math.ceil((this.total || 0) / this.pageSize) || 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
get pageStart() {
|
get pageStart() {
|
||||||
return computePageStart(this.total || 0, this.page, this.pageSize);
|
return this.total === 0 ? 0 : (this.page - 1) * this.pageSize + 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
get pageEnd() {
|
get pageEnd() {
|
||||||
return computePageEnd(this.total || 0, this.page, this.pageSize);
|
return this.total === 0 ? 0 : Math.min(this.page * this.pageSize, this.total);
|
||||||
}
|
}
|
||||||
|
|
||||||
get pageNumbers() {
|
get pageNumbers() {
|
||||||
return buildPageNumbers(this.page, this.totalPages);
|
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++) pages.push(i);
|
||||||
|
return pages;
|
||||||
}
|
}
|
||||||
|
|
||||||
// --------------------------
|
// --------------------------
|
||||||
|
|
@ -871,22 +795,4 @@ export class Faturamento implements AfterViewInit, OnDestroy {
|
||||||
const n = Number(value);
|
const n = Number(value);
|
||||||
return Number.isNaN(n) ? null : n;
|
return Number.isNaN(n) ? null : n;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async showToast(message: string): Promise<void> {
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -123,35 +123,11 @@
|
||||||
.btn-glass { border-radius: 12px; font-weight: 900; background: rgba(255, 255, 255, 0.6); border: 1px solid rgba(3, 15, 170, 0.25); color: var(--blue); &:hover { transform: translateY(-2px); border-color: var(--brand); background: #fff; } }
|
.btn-glass { border-radius: 12px; font-weight: 900; background: rgba(255, 255, 255, 0.6); border: 1px solid rgba(3, 15, 170, 0.25); color: var(--blue); &:hover { transform: translateY(-2px); border-color: var(--brand); background: #fff; } }
|
||||||
|
|
||||||
/* Filtros e Multi-Select */
|
/* Filtros e Multi-Select */
|
||||||
.filters-stack {
|
.filters-row { display: flex; justify-content: center; align-items: center; gap: 12px; flex-wrap: wrap; margin-top: 10px; }
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.filters-row {
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: flex-end;
|
|
||||||
gap: 12px;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
margin-top: 0;
|
|
||||||
position: relative;
|
|
||||||
z-index: 30;
|
|
||||||
overflow: visible;
|
|
||||||
}
|
|
||||||
|
|
||||||
.filters-row-top {
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.filters-row-bottom {
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
.filter-tabs { display: flex; gap: 4px; padding: 4px; background: rgba(255, 255, 255, 0.6); border: 1px solid rgba(17, 18, 20, 0.08); border-radius: 12px; backdrop-filter: blur(8px); }
|
.filter-tabs { display: flex; gap: 4px; padding: 4px; background: rgba(255, 255, 255, 0.6); border: 1px solid rgba(17, 18, 20, 0.08); border-radius: 12px; backdrop-filter: blur(8px); }
|
||||||
.filter-tab { border: none; background: transparent; padding: 8px 16px; border-radius: 8px; font-size: 0.85rem; font-weight: 700; color: var(--muted); transition: all 0.2s ease; display: flex; align-items: center; gap: 6px; &:hover { color: var(--text); background: rgba(255, 255, 255, 0.5); } &.active { background: #fff; color: var(--brand); box-shadow: 0 2px 8px rgba(227, 61, 207, 0.15); } &:disabled { opacity: 0.5; cursor: not-allowed; } }
|
.filter-tab { border: none; background: transparent; padding: 8px 16px; border-radius: 8px; font-size: 0.85rem; font-weight: 700; color: var(--muted); transition: all 0.2s ease; display: flex; align-items: center; gap: 6px; &:hover { color: var(--text); background: rgba(255, 255, 255, 0.5); } &.active { background: #fff; color: var(--brand); box-shadow: 0 2px 8px rgba(227, 61, 207, 0.15); } &:disabled { opacity: 0.5; cursor: not-allowed; } }
|
||||||
|
|
||||||
.client-filter-wrap { position: relative; z-index: 40; }
|
.client-filter-wrap { position: relative; }
|
||||||
.btn-client-filter { display: flex; align-items: center; gap: 8px; padding: 6px 12px; border-radius: 12px; border: 1px solid rgba(17, 18, 20, 0.08); background: rgba(255, 255, 255, 0.6); color: var(--muted); font-weight: 700; font-size: 0.85rem; backdrop-filter: blur(8px); transition: all 0.2s; min-height: 38px; height: auto; flex-wrap: wrap; &:hover { background: #fff; border-color: var(--blue); color: var(--blue); } &.active, &.has-selection { background: #fff; border-color: var(--brand); } }
|
.btn-client-filter { display: flex; align-items: center; gap: 8px; padding: 6px 12px; border-radius: 12px; border: 1px solid rgba(17, 18, 20, 0.08); background: rgba(255, 255, 255, 0.6); color: var(--muted); font-weight: 700; font-size: 0.85rem; backdrop-filter: blur(8px); transition: all 0.2s; min-height: 38px; height: auto; flex-wrap: wrap; &:hover { background: #fff; border-color: var(--blue); color: var(--blue); } &.active, &.has-selection { background: #fff; border-color: var(--brand); } }
|
||||||
.chips-container { display: flex; flex-wrap: wrap; gap: 6px; max-width: 400px; }
|
.chips-container { display: flex; flex-wrap: wrap; gap: 6px; max-width: 400px; }
|
||||||
.client-chip { display: inline-flex; align-items: center; background: rgba(227, 61, 207, 0.1); color: var(--brand); border: 1px solid rgba(227, 61, 207, 0.2); border-radius: 6px; padding: 2px 6px; font-size: 0.75rem; font-weight: 800; cursor: default; user-select: none; }
|
.client-chip { display: inline-flex; align-items: center; background: rgba(227, 61, 207, 0.1); color: var(--brand); border: 1px solid rgba(227, 61, 207, 0.2); border-radius: 6px; padding: 2px 6px; font-size: 0.75rem; font-weight: 800; cursor: default; user-select: none; }
|
||||||
|
|
@ -164,34 +140,6 @@
|
||||||
|
|
||||||
.additional-filter-wrap {
|
.additional-filter-wrap {
|
||||||
position: relative;
|
position: relative;
|
||||||
z-index: 40;
|
|
||||||
}
|
|
||||||
|
|
||||||
.operadora-empresa-filters {
|
|
||||||
display: flex;
|
|
||||||
align-items: flex-end;
|
|
||||||
gap: 10px;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
position: relative;
|
|
||||||
z-index: 50;
|
|
||||||
}
|
|
||||||
|
|
||||||
.filter-select-box {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 4px;
|
|
||||||
min-width: 190px;
|
|
||||||
position: relative;
|
|
||||||
z-index: 50;
|
|
||||||
}
|
|
||||||
|
|
||||||
.filter-select-label {
|
|
||||||
font-size: 0.66rem;
|
|
||||||
font-weight: 900;
|
|
||||||
letter-spacing: 0.05em;
|
|
||||||
text-transform: uppercase;
|
|
||||||
color: rgba(17, 18, 20, 0.58);
|
|
||||||
padding-left: 2px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-additional-filter {
|
.btn-additional-filter {
|
||||||
|
|
@ -301,18 +249,6 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 768px) {
|
|
||||||
.operadora-empresa-filters {
|
|
||||||
width: 100%;
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.filter-select-box {
|
|
||||||
flex: 1 1 220px;
|
|
||||||
min-width: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* KPIs */
|
/* KPIs */
|
||||||
.geral-kpis { display: grid; grid-template-columns: repeat(4, 1fr); gap: 12px; margin-top: 20px; margin-bottom: 16px; width: 100%; @media (max-width: 992px) { grid-template-columns: repeat(2, 1fr); } @media (max-width: 576px) { grid-template-columns: 1fr; } }
|
.geral-kpis { display: grid; grid-template-columns: repeat(4, 1fr); gap: 12px; margin-top: 20px; margin-bottom: 16px; width: 100%; @media (max-width: 992px) { grid-template-columns: repeat(2, 1fr); } @media (max-width: 576px) { grid-template-columns: 1fr; } }
|
||||||
.geral-kpis.geral-kpis-client {
|
.geral-kpis.geral-kpis-client {
|
||||||
|
|
@ -337,29 +273,6 @@
|
||||||
.controls { display: flex; gap: 12px; align-items: center; justify-content: space-between; flex-wrap: wrap; }
|
.controls { display: flex; gap: 12px; align-items: center; justify-content: space-between; flex-wrap: wrap; }
|
||||||
.search-group { 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); transition: all 0.2s ease; &:focus-within { border-color: var(--brand); box-shadow: 0 4px 12px rgba(227, 61, 207, 0.15); transform: translateY(-1px); } .input-group-text { background: transparent; border: none; color: var(--muted); padding-left: 14px; padding-right: 8px; display: flex; align-items: center; i { font-size: 1rem; } } .form-control { border: none; background: transparent; padding: 10px 0; font-size: 0.9rem; color: var(--text); box-shadow: none; &::placeholder { color: rgba(17, 18, 20, 0.4); font-weight: 500; } &:focus { outline: none; } } .btn-clear { background: transparent; border: none; color: var(--muted); padding: 0 12px; display: flex; align-items: center; cursor: pointer; transition: color 0.2s; &:hover { color: #dc3545; } i { font-size: 1rem; } } }
|
.search-group { 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); transition: all 0.2s ease; &:focus-within { border-color: var(--brand); box-shadow: 0 4px 12px rgba(227, 61, 207, 0.15); transform: translateY(-1px); } .input-group-text { background: transparent; border: none; color: var(--muted); padding-left: 14px; padding-right: 8px; display: flex; align-items: center; i { font-size: 1rem; } } .form-control { border: none; background: transparent; padding: 10px 0; font-size: 0.9rem; color: var(--text); box-shadow: none; &::placeholder { color: rgba(17, 18, 20, 0.4); font-weight: 500; } &:focus { outline: none; } } .btn-clear { background: transparent; border: none; color: var(--muted); padding: 0 12px; display: flex; align-items: center; cursor: pointer; transition: color 0.2s; &:hover { color: #dc3545; } i { font-size: 1rem; } } }
|
||||||
.page-size { margin-left: auto; @media (max-width: 500px) { margin-left: 0; width: 100%; justify-content: space-between; } }
|
.page-size { margin-left: auto; @media (max-width: 500px) { margin-left: 0; width: 100%; justify-content: space-between; } }
|
||||||
.batch-status-tools {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 8px;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
margin-left: auto;
|
|
||||||
|
|
||||||
@media (max-width: 900px) {
|
|
||||||
margin-left: 0;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.batch-status-count {
|
|
||||||
font-size: 0.75rem;
|
|
||||||
font-weight: 800;
|
|
||||||
text-transform: uppercase;
|
|
||||||
letter-spacing: 0.04em;
|
|
||||||
color: rgba(17, 18, 20, 0.62);
|
|
||||||
padding: 4px 8px;
|
|
||||||
border-radius: 999px;
|
|
||||||
border: 1px solid rgba(17, 18, 20, 0.12);
|
|
||||||
background: rgba(255, 255, 255, 0.8);
|
|
||||||
}
|
|
||||||
.select-wrapper { position: relative; display: inline-block; min-width: 90px; }
|
.select-wrapper { position: relative; display: inline-block; min-width: 90px; }
|
||||||
.select-glass { background: rgba(255, 255, 255, 0.7); border: 1px solid rgba(17, 18, 20, 0.15); border-radius: 12px; color: var(--blue); font-weight: 800; font-size: 0.9rem; text-align: left; padding: 8px 36px 8px 14px; box-shadow: 0 2px 6px rgba(0, 0, 0, 0.04); cursor: pointer; transition: all 0.2s ease; width: 100%; &:hover { background: #fff; border-color: var(--blue); transform: translateY(-1px); box-shadow: 0 4px 12px rgba(3, 15, 170, 0.1); } &:focus { outline: none; border-color: var(--brand); box-shadow: 0 0 0 3px rgba(227, 61, 207, 0.15); } }
|
.select-glass { background: rgba(255, 255, 255, 0.7); border: 1px solid rgba(17, 18, 20, 0.15); border-radius: 12px; color: var(--blue); font-weight: 800; font-size: 0.9rem; text-align: left; padding: 8px 36px 8px 14px; box-shadow: 0 2px 6px rgba(0, 0, 0, 0.04); cursor: pointer; transition: all 0.2s ease; width: 100%; &:hover { background: #fff; border-color: var(--blue); transform: translateY(-1px); box-shadow: 0 4px 12px rgba(3, 15, 170, 0.1); } &:focus { outline: none; border-color: var(--brand); box-shadow: 0 0 0 3px rgba(227, 61, 207, 0.15); } }
|
||||||
.select-icon { position: absolute; right: 12px; top: 50%; transform: translateY(-50%); pointer-events: none; color: var(--muted); font-size: 0.75rem; transition: transform 0.2s ease; }
|
.select-icon { position: absolute; right: 12px; top: 50%; transform: translateY(-50%); pointer-events: none; color: var(--muted); font-size: 0.75rem; transition: transform 0.2s ease; }
|
||||||
|
|
@ -587,6 +500,8 @@
|
||||||
/* ========================================================== */
|
/* ========================================================== */
|
||||||
/* 8. MODALS E FORMULÁRIOS COMPLETOS (RESTAURADOS ✅) */
|
/* 8. MODALS E FORMULÁRIOS COMPLETOS (RESTAURADOS ✅) */
|
||||||
/* ========================================================== */
|
/* ========================================================== */
|
||||||
|
.modal-backdrop-custom { position: fixed; inset: 0; background: rgba(0,0,0,0.45); z-index: 9990; backdrop-filter: blur(4px); }
|
||||||
|
.modal-custom { position: fixed; inset: 0; display: flex; align-items: center; justify-content: center; z-index: 9995; padding: 16px; }
|
||||||
.modal-card { background: #ffffff; border: 1px solid rgba(255,255,255,0.8); border-radius: 20px; box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25); overflow: hidden; display: flex; flex-direction: column; animation: modalPop 0.3s cubic-bezier(0.34, 1.56, 0.64, 1); width: min(850px, 100%); max-height: 90vh; }
|
.modal-card { background: #ffffff; border: 1px solid rgba(255,255,255,0.8); border-radius: 20px; box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25); overflow: hidden; display: flex; flex-direction: column; animation: modalPop 0.3s cubic-bezier(0.34, 1.56, 0.64, 1); width: min(850px, 100%); max-height: 90vh; }
|
||||||
@keyframes modalPop { from { opacity: 0; transform: scale(0.95) translateY(10px); } to { opacity: 1; transform: scale(1) translateY(0); } }
|
@keyframes modalPop { from { opacity: 0; transform: scale(0.95) translateY(10px); } to { opacity: 1; transform: scale(1) translateY(0); } }
|
||||||
|
|
||||||
|
|
@ -603,18 +518,12 @@
|
||||||
.modal-body .box-body { overflow: visible; }
|
.modal-body .box-body { overflow: visible; }
|
||||||
.modal-xl-custom { width: min(1100px, 95vw); max-height: 85vh; }
|
.modal-xl-custom { width: min(1100px, 95vw); max-height: 85vh; }
|
||||||
.modal-card.modal-client-detail {
|
.modal-card.modal-client-detail {
|
||||||
width: min(980px, 96vw);
|
width: min(560px, 95vw);
|
||||||
}
|
}
|
||||||
.modal-card.modal-client-detail .details-dashboard {
|
.modal-card.modal-client-detail .details-dashboard {
|
||||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
grid-template-columns: 1fr;
|
||||||
gap: 14px;
|
max-width: 520px;
|
||||||
max-width: 940px;
|
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
|
|
||||||
@media (max-width: 900px) {
|
|
||||||
grid-template-columns: 1fr;
|
|
||||||
max-width: 520px;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
.modal-card.modal-create { width: min(1280px, 96vw); max-height: 92vh; }
|
.modal-card.modal-create { width: min(1280px, 96vw); max-height: 92vh; }
|
||||||
.modal-card.modal-create.batch-mode { width: min(1560px, 99vw); }
|
.modal-card.modal-create.batch-mode { width: min(1560px, 99vw); }
|
||||||
|
|
@ -666,140 +575,6 @@
|
||||||
padding: 20px 22px;
|
padding: 20px 22px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.modal-card.modal-batch-status {
|
|
||||||
width: min(1120px, 96vw);
|
|
||||||
max-height: 92vh;
|
|
||||||
|
|
||||||
.modal-header {
|
|
||||||
padding: 18px 22px 16px;
|
|
||||||
align-items: flex-start;
|
|
||||||
gap: 12px;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.modal-title {
|
|
||||||
flex: 1 1 320px;
|
|
||||||
min-width: 0;
|
|
||||||
line-height: 1.2;
|
|
||||||
}
|
|
||||||
|
|
||||||
.batch-status-header-actions {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: flex-end;
|
|
||||||
gap: 8px;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
margin-left: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.details-dashboard {
|
|
||||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
||||||
gap: 16px;
|
|
||||||
align-items: start;
|
|
||||||
|
|
||||||
@media (max-width: 980px) {
|
|
||||||
grid-template-columns: 1fr;
|
|
||||||
gap: 12px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.modal-body {
|
|
||||||
padding: 18px 22px 22px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.detail-box {
|
|
||||||
height: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.box-body {
|
|
||||||
padding: 14px 16px 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-grid {
|
|
||||||
gap: 14px 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-field {
|
|
||||||
gap: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.reserva-confirmation-pills {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: flex-start;
|
|
||||||
gap: 10px;
|
|
||||||
margin-top: 2px;
|
|
||||||
|
|
||||||
.summary-pill {
|
|
||||||
margin: 0;
|
|
||||||
white-space: normal;
|
|
||||||
line-height: 1.25;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.batch-status-note {
|
|
||||||
display: block;
|
|
||||||
margin-top: 10px;
|
|
||||||
font-size: 0.8rem;
|
|
||||||
line-height: 1.4;
|
|
||||||
color: rgba(17, 18, 20, 0.58);
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 980px) {
|
|
||||||
.modal-header {
|
|
||||||
padding: 16px 18px 14px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.batch-status-header-actions {
|
|
||||||
width: 100%;
|
|
||||||
justify-content: flex-end;
|
|
||||||
}
|
|
||||||
|
|
||||||
.modal-body {
|
|
||||||
padding: 16px 18px 18px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-grid {
|
|
||||||
gap: 12px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 640px) {
|
|
||||||
.modal-header {
|
|
||||||
padding: 14px 14px 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.modal-title {
|
|
||||||
font-size: 1rem;
|
|
||||||
gap: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.batch-status-header-actions {
|
|
||||||
justify-content: stretch;
|
|
||||||
|
|
||||||
.btn {
|
|
||||||
flex: 1 1 140px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.modal-body {
|
|
||||||
padding: 14px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.box-body {
|
|
||||||
padding: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.reserva-confirmation-pills {
|
|
||||||
gap: 8px;
|
|
||||||
|
|
||||||
.summary-pill {
|
|
||||||
width: 100%;
|
|
||||||
justify-content: flex-start;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* === MODAL DE EDITAR E SEÇÕES (Accordion) === */
|
/* === MODAL DE EDITAR E SEÇÕES (Accordion) === */
|
||||||
/* ✅ Restauração Crítica para "Editar" e "Novo Cliente" */
|
/* ✅ Restauração Crítica para "Editar" e "Novo Cliente" */
|
||||||
|
|
@ -930,421 +705,3 @@ div.box-body { padding: 16px; &.compact { padding: 12px 16px; } &.compact-paddin
|
||||||
.summary-pill { font-size: 0.72rem; }
|
.summary-pill { font-size: 0.72rem; }
|
||||||
.batch-validation-banner { align-items: flex-start; }
|
.batch-validation-banner { align-items: flex-start; }
|
||||||
}
|
}
|
||||||
|
|
||||||
.modal-mve-audit {
|
|
||||||
width: min(1320px, calc(100vw - 40px));
|
|
||||||
max-width: min(1320px, calc(100vw - 40px));
|
|
||||||
max-height: calc(100vh - 48px);
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mve-audit-body {
|
|
||||||
display: grid;
|
|
||||||
gap: 16px;
|
|
||||||
overflow: auto;
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mve-intro-card,
|
|
||||||
.mve-upload-card {
|
|
||||||
background: linear-gradient(180deg, rgba(255,255,255,0.98), rgba(247,249,252,0.98));
|
|
||||||
border: 1px solid rgba(17,18,20,0.08);
|
|
||||||
border-radius: 18px;
|
|
||||||
padding: 18px;
|
|
||||||
box-shadow: 0 16px 32px rgba(17,18,20,0.05);
|
|
||||||
}
|
|
||||||
|
|
||||||
.mve-intro-card {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
gap: 16px;
|
|
||||||
align-items: flex-start;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mve-intro-title {
|
|
||||||
font-size: 1rem;
|
|
||||||
font-weight: 900;
|
|
||||||
color: var(--text);
|
|
||||||
}
|
|
||||||
|
|
||||||
.mve-intro-text {
|
|
||||||
margin-top: 6px;
|
|
||||||
color: rgba(17,18,20,0.68);
|
|
||||||
max-width: 760px;
|
|
||||||
line-height: 1.45;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mve-intro-meta,
|
|
||||||
.mve-summary-notes {
|
|
||||||
display: flex;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
gap: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.meta-pill {
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 6px;
|
|
||||||
padding: 7px 12px;
|
|
||||||
border-radius: 999px;
|
|
||||||
background: rgba(3,15,170,0.06);
|
|
||||||
color: rgba(17,18,20,0.76);
|
|
||||||
font-size: 0.76rem;
|
|
||||||
font-weight: 800;
|
|
||||||
border: 1px solid rgba(3,15,170,0.08);
|
|
||||||
|
|
||||||
&.accent {
|
|
||||||
background: rgba(12,132,78,0.08);
|
|
||||||
border-color: rgba(12,132,78,0.16);
|
|
||||||
color: #0c6c43;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.mve-section-title {
|
|
||||||
font-size: 0.94rem;
|
|
||||||
font-weight: 900;
|
|
||||||
color: var(--text);
|
|
||||||
margin-bottom: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mve-upload-zone {
|
|
||||||
display: grid;
|
|
||||||
gap: 8px;
|
|
||||||
justify-items: center;
|
|
||||||
text-align: center;
|
|
||||||
border: 1.5px dashed rgba(3,15,170,0.22);
|
|
||||||
border-radius: 18px;
|
|
||||||
padding: 28px 18px;
|
|
||||||
background:
|
|
||||||
radial-gradient(circle at top right, rgba(3,15,170,0.08), transparent 36%),
|
|
||||||
linear-gradient(180deg, rgba(255,255,255,0.96), rgba(243,247,255,0.92));
|
|
||||||
cursor: pointer;
|
|
||||||
transition: border-color 0.18s ease, transform 0.18s ease;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
border-color: rgba(3,15,170,0.38);
|
|
||||||
transform: translateY(-1px);
|
|
||||||
}
|
|
||||||
|
|
||||||
input {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.upload-icon {
|
|
||||||
width: 54px;
|
|
||||||
height: 54px;
|
|
||||||
border-radius: 16px;
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
background: rgba(3,15,170,0.08);
|
|
||||||
color: var(--blue);
|
|
||||||
font-size: 1.4rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.upload-title {
|
|
||||||
font-size: 0.96rem;
|
|
||||||
font-weight: 900;
|
|
||||||
color: var(--text);
|
|
||||||
}
|
|
||||||
|
|
||||||
.upload-subtitle,
|
|
||||||
.processing-text,
|
|
||||||
.mve-upload-meta {
|
|
||||||
color: rgba(17,18,20,0.62);
|
|
||||||
font-size: 0.82rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mve-upload-actions,
|
|
||||||
.mve-actions-row,
|
|
||||||
.confirm-actions {
|
|
||||||
display: flex;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
gap: 10px;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mve-upload-actions {
|
|
||||||
margin-top: 14px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mve-processing {
|
|
||||||
display: grid;
|
|
||||||
gap: 10px;
|
|
||||||
margin-top: 14px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.progress-track {
|
|
||||||
width: 100%;
|
|
||||||
height: 8px;
|
|
||||||
border-radius: 999px;
|
|
||||||
background: rgba(17,18,20,0.08);
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.progress-indeterminate {
|
|
||||||
display: block;
|
|
||||||
width: 34%;
|
|
||||||
height: 100%;
|
|
||||||
border-radius: inherit;
|
|
||||||
background: linear-gradient(90deg, #030faa, #2b55e3);
|
|
||||||
animation: mve-progress 1.25s linear infinite;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes mve-progress {
|
|
||||||
from { transform: translateX(-120%); }
|
|
||||||
to { transform: translateX(320%); }
|
|
||||||
}
|
|
||||||
|
|
||||||
.mve-summary-grid {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(7, minmax(0, 1fr));
|
|
||||||
gap: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mve-summary-card {
|
|
||||||
background: #fff;
|
|
||||||
border: 1px solid rgba(17,18,20,0.07);
|
|
||||||
border-radius: 18px;
|
|
||||||
padding: 16px 14px;
|
|
||||||
display: grid;
|
|
||||||
gap: 6px;
|
|
||||||
box-shadow: 0 12px 24px rgba(17,18,20,0.05);
|
|
||||||
|
|
||||||
strong {
|
|
||||||
font-size: 1.36rem;
|
|
||||||
color: var(--text);
|
|
||||||
}
|
|
||||||
|
|
||||||
&.is-positive strong { color: #198754; }
|
|
||||||
&.is-danger strong { color: #b42318; }
|
|
||||||
&.is-warning strong { color: #b54708; }
|
|
||||||
}
|
|
||||||
|
|
||||||
.summary-label {
|
|
||||||
font-size: 0.76rem;
|
|
||||||
letter-spacing: 0.03em;
|
|
||||||
text-transform: uppercase;
|
|
||||||
color: rgba(17,18,20,0.58);
|
|
||||||
font-weight: 900;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mve-result-toolbar {
|
|
||||||
display: flex;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
justify-content: space-between;
|
|
||||||
gap: 14px;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mve-filter-tabs {
|
|
||||||
display: flex;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
gap: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mve-toolbar-right {
|
|
||||||
display: flex;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
gap: 12px;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mve-search-group {
|
|
||||||
min-width: min(420px, 100%);
|
|
||||||
}
|
|
||||||
|
|
||||||
.mve-sync-banner {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
gap: 12px;
|
|
||||||
align-items: center;
|
|
||||||
padding: 14px 16px;
|
|
||||||
border-radius: 16px;
|
|
||||||
background: rgba(12,132,78,0.08);
|
|
||||||
border: 1px solid rgba(12,132,78,0.16);
|
|
||||||
color: #0c6c43;
|
|
||||||
font-weight: 700;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mve-empty {
|
|
||||||
display: grid;
|
|
||||||
place-items: center;
|
|
||||||
gap: 8px;
|
|
||||||
min-height: 180px;
|
|
||||||
border-radius: 18px;
|
|
||||||
border: 1px dashed rgba(17,18,20,0.14);
|
|
||||||
background: rgba(255,255,255,0.72);
|
|
||||||
color: rgba(17,18,20,0.62);
|
|
||||||
|
|
||||||
i {
|
|
||||||
font-size: 1.5rem;
|
|
||||||
color: #198754;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.mve-table-wrap {
|
|
||||||
max-height: min(48vh, 560px);
|
|
||||||
border-radius: 18px;
|
|
||||||
background: #fff;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mve-table {
|
|
||||||
min-width: 980px;
|
|
||||||
|
|
||||||
thead th {
|
|
||||||
position: sticky;
|
|
||||||
top: 0;
|
|
||||||
z-index: 1;
|
|
||||||
background: rgba(248,249,250,0.97);
|
|
||||||
font-size: 0.73rem;
|
|
||||||
text-transform: uppercase;
|
|
||||||
letter-spacing: 0.04em;
|
|
||||||
color: rgba(17,18,20,0.64);
|
|
||||||
font-weight: 900;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.mve-status-pair,
|
|
||||||
.mve-cell-stack {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 6px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mve-status-pair {
|
|
||||||
.bi-arrow-right {
|
|
||||||
font-size: 0.8rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.mve-issue-tag,
|
|
||||||
.mve-severity {
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
align-self: flex-start;
|
|
||||||
padding: 6px 10px;
|
|
||||||
border-radius: 999px;
|
|
||||||
font-size: 0.74rem;
|
|
||||||
font-weight: 900;
|
|
||||||
border: 1px solid transparent;
|
|
||||||
|
|
||||||
&.status { background: rgba(180,35,24,0.09); color: #b42318; border-color: rgba(180,35,24,0.16); }
|
|
||||||
&.data { background: rgba(181,71,8,0.09); color: #b54708; border-color: rgba(181,71,8,0.16); }
|
|
||||||
&.system { background: rgba(3,15,170,0.08); color: var(--blue); border-color: rgba(3,15,170,0.15); }
|
|
||||||
&.report { background: rgba(10,91,168,0.08); color: #0a5ba8; border-color: rgba(10,91,168,0.15); }
|
|
||||||
&.duplicate { background: rgba(113,46,170,0.08); color: #712eaa; border-color: rgba(113,46,170,0.14); }
|
|
||||||
&.warning { background: rgba(245,158,11,0.12); color: #9a6700; border-color: rgba(245,158,11,0.16); }
|
|
||||||
&.neutral { background: rgba(17,18,20,0.06); color: rgba(17,18,20,0.72); border-color: rgba(17,18,20,0.1); }
|
|
||||||
}
|
|
||||||
|
|
||||||
.mve-severity {
|
|
||||||
&.critical { background: rgba(180,35,24,0.08); color: #b42318; border-color: rgba(180,35,24,0.15); }
|
|
||||||
&.medium { background: rgba(181,71,8,0.08); color: #b54708; border-color: rgba(181,71,8,0.14); }
|
|
||||||
&.warning { background: rgba(245,158,11,0.12); color: #9a6700; border-color: rgba(245,158,11,0.18); }
|
|
||||||
&.neutral { background: rgba(17,18,20,0.06); color: rgba(17,18,20,0.65); border-color: rgba(17,18,20,0.1); }
|
|
||||||
}
|
|
||||||
|
|
||||||
.mve-differences-text {
|
|
||||||
color: rgba(17,18,20,0.78);
|
|
||||||
line-height: 1.38;
|
|
||||||
min-width: 260px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mve-footer {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
gap: 12px;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mve-actions-row {
|
|
||||||
justify-content: flex-end;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mve-confirm-overlay {
|
|
||||||
position: sticky;
|
|
||||||
bottom: 0;
|
|
||||||
display: flex;
|
|
||||||
justify-content: flex-end;
|
|
||||||
padding-top: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mve-confirm-card {
|
|
||||||
width: min(420px, 100%);
|
|
||||||
background: linear-gradient(180deg, rgba(255,255,255,0.98), rgba(245,248,255,0.98));
|
|
||||||
border: 1px solid rgba(3,15,170,0.12);
|
|
||||||
border-radius: 18px;
|
|
||||||
padding: 18px;
|
|
||||||
box-shadow: 0 18px 36px rgba(17,18,20,0.12);
|
|
||||||
display: grid;
|
|
||||||
gap: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.confirm-title {
|
|
||||||
font-size: 1rem;
|
|
||||||
font-weight: 900;
|
|
||||||
color: var(--text);
|
|
||||||
}
|
|
||||||
|
|
||||||
.confirm-text,
|
|
||||||
.confirm-footnote {
|
|
||||||
color: rgba(17,18,20,0.68);
|
|
||||||
line-height: 1.4;
|
|
||||||
}
|
|
||||||
|
|
||||||
.confirm-summary {
|
|
||||||
display: grid;
|
|
||||||
gap: 6px;
|
|
||||||
padding: 12px 14px;
|
|
||||||
border-radius: 14px;
|
|
||||||
background: rgba(3,15,170,0.05);
|
|
||||||
border: 1px solid rgba(3,15,170,0.08);
|
|
||||||
font-weight: 700;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 1200px) {
|
|
||||||
.mve-summary-grid {
|
|
||||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 768px) {
|
|
||||||
.modal-mve-audit {
|
|
||||||
width: calc(100vw - 20px);
|
|
||||||
max-width: calc(100vw - 20px);
|
|
||||||
max-height: calc(100vh - 20px);
|
|
||||||
}
|
|
||||||
|
|
||||||
.mve-intro-card,
|
|
||||||
.mve-result-toolbar,
|
|
||||||
.mve-footer,
|
|
||||||
.mve-sync-banner {
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: stretch;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mve-summary-grid {
|
|
||||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
||||||
}
|
|
||||||
|
|
||||||
.mve-toolbar-right,
|
|
||||||
.mve-actions-row,
|
|
||||||
.confirm-actions {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mve-toolbar-right > *,
|
|
||||||
.mve-actions-row .btn,
|
|
||||||
.confirm-actions .btn,
|
|
||||||
.mve-upload-actions .btn {
|
|
||||||
flex: 1 1 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mve-search-group {
|
|
||||||
min-width: 100%;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -73,36 +73,4 @@ describe('Geral', () => {
|
||||||
expect(component.createBatchLines[0].linha).toBe('11888888888');
|
expect(component.createBatchLines[0].linha).toBe('11888888888');
|
||||||
expect(component.createBatchLines[0]['planoContrato']).toBe('PLANO B');
|
expect(component.createBatchLines[0]['planoContrato']).toBe('PLANO B');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should apply TIM filter in client-side pipeline using conta TIM textual', () => {
|
|
||||||
component.filterOperadora = 'TIM';
|
|
||||||
component.filterContaEmpresa = '';
|
|
||||||
component.filterStatus = 'ALL';
|
|
||||||
component.additionalMode = 'ALL';
|
|
||||||
component.selectedAdditionalServices = [];
|
|
||||||
|
|
||||||
const filtered = (component as any).applyAdditionalFiltersClientSide([
|
|
||||||
{ id: '1', item: 1, conta: 'TIM', linha: '11911111111', cliente: 'A', usuario: 'U', vencConta: null, status: 'ATIVO' },
|
|
||||||
{ id: '2', item: 2, conta: '455371844', linha: '11922222222', cliente: 'B', usuario: 'U', vencConta: null, status: 'ATIVO' },
|
|
||||||
]);
|
|
||||||
|
|
||||||
expect(filtered.length).toBe(1);
|
|
||||||
expect(filtered[0].conta).toBe('TIM');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should combine operadora and empresa filters for VIVO MACROPHONY', () => {
|
|
||||||
component.filterOperadora = 'VIVO';
|
|
||||||
component.filterContaEmpresa = 'VIVO MACROPHONY';
|
|
||||||
component.filterStatus = 'ALL';
|
|
||||||
component.additionalMode = 'ALL';
|
|
||||||
component.selectedAdditionalServices = [];
|
|
||||||
|
|
||||||
const filtered = (component as any).applyAdditionalFiltersClientSide([
|
|
||||||
{ id: '1', item: 1, conta: '460161507', linha: '11911111111', cliente: 'A', usuario: 'U', vencConta: null, status: 'ATIVO' },
|
|
||||||
{ id: '2', item: 2, conta: '0435288088', linha: '11922222222', cliente: 'B', usuario: 'U', vencConta: null, status: 'ATIVO' },
|
|
||||||
]);
|
|
||||||
|
|
||||||
expect(filtered.length).toBe(1);
|
|
||||||
expect(filtered[0].conta).toBe('460161507');
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -1,274 +0,0 @@
|
||||||
<div class="toast-container position-fixed top-0 end-0 p-3" style="z-index: 10000;">
|
|
||||||
<div #successToast class="toast text-bg-danger border-0 shadow" role="alert" aria-live="assertive" aria-atomic="true">
|
|
||||||
<div class="toast-header border-bottom-0">
|
|
||||||
<strong class="me-auto text-primary">LineGestão</strong>
|
|
||||||
<button type="button" class="btn-close" data-bs-dismiss="toast" aria-label="Fechar"></button>
|
|
||||||
</div>
|
|
||||||
<div class="toast-body bg-white rounded-bottom text-dark">
|
|
||||||
{{ toastMessage }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<section class="historico-linhas-page historico-chips-page">
|
|
||||||
<span class="page-blob blob-1" aria-hidden="true"></span>
|
|
||||||
<span class="page-blob blob-2" aria-hidden="true"></span>
|
|
||||||
<span class="page-blob blob-3" aria-hidden="true"></span>
|
|
||||||
<span class="page-blob blob-4" aria-hidden="true"></span>
|
|
||||||
|
|
||||||
<div class="container-geral-responsive">
|
|
||||||
<div class="geral-card">
|
|
||||||
<div class="geral-header">
|
|
||||||
<div class="header-row-top">
|
|
||||||
<div class="title-badge">
|
|
||||||
<i class="bi bi-sim"></i> Chip
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="header-title">
|
|
||||||
<h5 class="title mb-0">Histórico de Chips</h5>
|
|
||||||
<small class="subtitle">Timeline das alterações feitas em um chip específico.</small>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="header-actions d-flex gap-2 justify-content-end">
|
|
||||||
<button type="button" class="btn btn-brand btn-sm" (click)="refresh()" [disabled]="loading">
|
|
||||||
<i class="bi bi-arrow-clockwise me-1"></i> Atualizar
|
|
||||||
</button>
|
|
||||||
<button type="button" class="btn btn-glass btn-sm" (click)="onExport()" [disabled]="loading || exporting">
|
|
||||||
<span *ngIf="!exporting"><i class="bi bi-download me-1"></i> Exportar</span>
|
|
||||||
<span *ngIf="exporting"><span class="spinner-border spinner-border-sm me-2"></span> Exportando...</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="filters-card mt-4">
|
|
||||||
<div class="filters-head">
|
|
||||||
<div class="filters-title">
|
|
||||||
<i class="bi bi-funnel"></i>
|
|
||||||
<span>Filtros</span>
|
|
||||||
</div>
|
|
||||||
<div class="filters-actions">
|
|
||||||
<button class="btn-primary" type="button" (click)="applyFilters()" [disabled]="loading">
|
|
||||||
<i class="bi bi-check2"></i> Aplicar
|
|
||||||
</button>
|
|
||||||
<button class="btn-ghost" type="button" (click)="clearFilters()" [disabled]="loading">
|
|
||||||
<i class="bi bi-x-circle"></i> Limpar
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="filters-grid">
|
|
||||||
<div class="filter-field line-field">
|
|
||||||
<label>Chip</label>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
inputmode="numeric"
|
|
||||||
placeholder="Opcional"
|
|
||||||
[(ngModel)]="filterChip"
|
|
||||||
[disabled]="loading"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="filter-field">
|
|
||||||
<label>Origem</label>
|
|
||||||
<app-select
|
|
||||||
class="select-glass"
|
|
||||||
size="sm"
|
|
||||||
[options]="pageOptions"
|
|
||||||
labelKey="label"
|
|
||||||
valueKey="value"
|
|
||||||
placeholder="Todas"
|
|
||||||
[(ngModel)]="filterPageName"
|
|
||||||
[disabled]="loading">
|
|
||||||
</app-select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="filter-field">
|
|
||||||
<label>Ação</label>
|
|
||||||
<app-select
|
|
||||||
class="select-glass"
|
|
||||||
size="sm"
|
|
||||||
[options]="actionOptions"
|
|
||||||
labelKey="label"
|
|
||||||
valueKey="value"
|
|
||||||
placeholder="Todas"
|
|
||||||
[(ngModel)]="filterAction"
|
|
||||||
[disabled]="loading">
|
|
||||||
</app-select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="filter-field">
|
|
||||||
<label>Usuário</label>
|
|
||||||
<input type="text" placeholder="Nome ou e-mail" [(ngModel)]="filterUser" [disabled]="loading" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="filter-field period-field">
|
|
||||||
<label>Período (De)</label>
|
|
||||||
<input type="date" [(ngModel)]="dateFrom" [disabled]="loading" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="filter-field period-field">
|
|
||||||
<label>Período (Até)</label>
|
|
||||||
<input type="date" [(ngModel)]="dateTo" [disabled]="loading" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="kpi-grid mt-3" *ngIf="logs.length > 0">
|
|
||||||
<div class="kpi-card">
|
|
||||||
<span class="kpi-label">Eventos (filtro)</span>
|
|
||||||
<strong class="kpi-value">{{ total }}</strong>
|
|
||||||
</div>
|
|
||||||
<div class="kpi-card">
|
|
||||||
<span class="kpi-label">Trocas de Chip (página)</span>
|
|
||||||
<strong class="kpi-value">{{ chipCountInPage }}</strong>
|
|
||||||
</div>
|
|
||||||
<div class="kpi-card">
|
|
||||||
<span class="kpi-label">Trocas de Número (página)</span>
|
|
||||||
<strong class="kpi-value">{{ trocaCountInPage }}</strong>
|
|
||||||
</div>
|
|
||||||
<div class="kpi-card">
|
|
||||||
<span class="kpi-label">Status (página)</span>
|
|
||||||
<strong class="kpi-value">{{ statusCountInPage }}</strong>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="geral-body">
|
|
||||||
<div class="table-wrap">
|
|
||||||
<div class="text-center p-5" *ngIf="loading">
|
|
||||||
<span class="spinner-border text-brand"></span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="alert alert-danger m-4" role="alert" *ngIf="!loading && error">
|
|
||||||
{{ errorMsg || 'Erro ao carregar histórico do chip.' }}
|
|
||||||
<button class="btn btn-sm btn-outline-danger ms-3" type="button" (click)="refresh()">Tentar novamente</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="empty-group" *ngIf="!loading && !error && logs.length === 0">
|
|
||||||
Nenhuma alteração encontrada para os filtros informados.
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<table class="table table-modern align-middle mb-0" *ngIf="!loading && !error && logs.length > 0">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>Data/Hora</th>
|
|
||||||
<th>Usuário</th>
|
|
||||||
<th>Origem</th>
|
|
||||||
<th>Ação</th>
|
|
||||||
<th>Resumo da alteração</th>
|
|
||||||
<th class="actions-col">Detalhes</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
<ng-container *ngFor="let log of logs; trackBy: trackByLog">
|
|
||||||
<tr class="table-row-item" [class.expanded]="expandedLogId === log.id">
|
|
||||||
<td class="fw-bold text-muted">{{ formatDateTime(log.occurredAtUtc) }}</td>
|
|
||||||
<td>
|
|
||||||
<div class="user-cell">
|
|
||||||
<span class="user-name">{{ displayUserName(log) }}</span>
|
|
||||||
<small class="user-email">{{ log.userEmail || '-' }}</small>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
<span class="origin-pill">{{ log.page || '-' }}</span>
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
<span class="badge-action" [ngClass]="actionClass(log.action)">{{ formatAction(log.action) }}</span>
|
|
||||||
</td>
|
|
||||||
<td class="summary-col">
|
|
||||||
<ng-container *ngIf="summaryFor(log) as summary">
|
|
||||||
<div class="summary-title" [ngClass]="toneClass(summary.tone)">{{ summary.title }}</div>
|
|
||||||
<div class="summary-description">{{ summary.description }}</div>
|
|
||||||
<div class="summary-diff" *ngIf="summary.before || summary.after">
|
|
||||||
<span class="old">{{ formatChangeValue(summary.before) }}</span>
|
|
||||||
<i class="bi bi-arrow-right"></i>
|
|
||||||
<span class="new">{{ formatChangeValue(summary.after) }}</span>
|
|
||||||
</div>
|
|
||||||
<div class="summary-ddd" *ngIf="summary.beforeDdd || summary.afterDdd">
|
|
||||||
DDD: {{ formatChangeValue(summary.beforeDdd) }} <i class="bi bi-arrow-right"></i> {{ formatChangeValue(summary.afterDdd) }}
|
|
||||||
</div>
|
|
||||||
</ng-container>
|
|
||||||
</td>
|
|
||||||
<td class="actions-col">
|
|
||||||
<button
|
|
||||||
class="expand-btn"
|
|
||||||
type="button"
|
|
||||||
(click)="toggleDetails(log, $event)"
|
|
||||||
[attr.aria-expanded]="expandedLogId === log.id"
|
|
||||||
[attr.aria-label]="expandedLogId === log.id ? 'Fechar detalhes' : 'Abrir detalhes'">
|
|
||||||
<i class="bi" [class.bi-chevron-down]="expandedLogId !== log.id" [class.bi-chevron-up]="expandedLogId === log.id"></i>
|
|
||||||
</button>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<tr class="details-row" *ngIf="expandedLogId === log.id">
|
|
||||||
<td colspan="6">
|
|
||||||
<div class="details-panel">
|
|
||||||
<div class="details-section">
|
|
||||||
<div class="section-title">
|
|
||||||
<i class="bi bi-pencil-square"></i> Mudanças de campos
|
|
||||||
</div>
|
|
||||||
<ng-container *ngIf="visibleChanges(log) as changes">
|
|
||||||
<div class="changes-list" *ngIf="changes.length; else noChanges">
|
|
||||||
<div class="change-item" *ngFor="let change of changes; trackBy: trackByField">
|
|
||||||
<div class="change-head">
|
|
||||||
<span class="change-field">{{ change.field }}</span>
|
|
||||||
<span class="change-type" [ngClass]="changeTypeClass(change.changeType)">
|
|
||||||
{{ changeTypeLabel(change.changeType) }}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div class="change-values">
|
|
||||||
<span class="old">{{ formatChangeValue(change.oldValue) }}</span>
|
|
||||||
<i class="bi bi-arrow-right"></i>
|
|
||||||
<span class="new">{{ formatChangeValue(change.newValue) }}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</ng-container>
|
|
||||||
<ng-template #noChanges>
|
|
||||||
<div class="empty-state">Sem mudanças detalhadas nesse evento.</div>
|
|
||||||
</ng-template>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</ng-container>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="geral-footer">
|
|
||||||
<div class="footer-meta">
|
|
||||||
<div class="small text-muted fw-bold">Mostrando {{ pageStart }}–{{ pageEnd }} de {{ total }} registros</div>
|
|
||||||
<div class="page-size d-flex align-items-center gap-2">
|
|
||||||
<span class="text-muted small fw-bold text-uppercase" style="letter-spacing: 0.5px; font-size: 0.75rem;">Itens por pág:</span>
|
|
||||||
<div class="select-wrapper">
|
|
||||||
<app-select
|
|
||||||
class="select-glass"
|
|
||||||
size="sm"
|
|
||||||
[options]="pageSizeOptions"
|
|
||||||
[(ngModel)]="pageSize"
|
|
||||||
(ngModelChange)="onPageSizeChange()"
|
|
||||||
[disabled]="loading">
|
|
||||||
</app-select>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<nav>
|
|
||||||
<ul class="pagination pagination-sm mb-0 pagination-modern">
|
|
||||||
<li class="page-item" [class.disabled]="page === 1 || loading">
|
|
||||||
<button class="page-link" (click)="goToPage(page - 1)">Anterior</button>
|
|
||||||
</li>
|
|
||||||
<li class="page-item" *ngFor="let p of pageNumbers" [class.active]="p === page">
|
|
||||||
<button class="page-link" (click)="goToPage(p)">{{ p }}</button>
|
|
||||||
</li>
|
|
||||||
<li class="page-item" [class.disabled]="page === totalPages || loading">
|
|
||||||
<button class="page-link" (click)="goToPage(page + 1)">Próxima</button>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</nav>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
@ -1,554 +0,0 @@
|
||||||
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,
|
|
||||||
ChipHistoricoQuery
|
|
||||||
} from '../../services/historico.service';
|
|
||||||
import { TableExportService } from '../../services/table-export.service';
|
|
||||||
import {
|
|
||||||
buildPageNumbers,
|
|
||||||
clampPage,
|
|
||||||
computePageEnd,
|
|
||||||
computePageStart,
|
|
||||||
computeTotalPages
|
|
||||||
} from '../../utils/pagination.util';
|
|
||||||
|
|
||||||
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-chips',
|
|
||||||
standalone: true,
|
|
||||||
imports: [CommonModule, FormsModule, CustomSelectComponent],
|
|
||||||
templateUrl: './historico-chips.html',
|
|
||||||
styleUrls: ['../historico-linhas/historico-linhas.scss'],
|
|
||||||
})
|
|
||||||
export class HistoricoChips 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;
|
|
||||||
|
|
||||||
filterChip = '';
|
|
||||||
filterPageName = '';
|
|
||||||
filterAction = '';
|
|
||||||
filterUser = '';
|
|
||||||
dateFrom = '';
|
|
||||||
dateTo = '';
|
|
||||||
|
|
||||||
readonly pageOptions: SelectOption[] = [
|
|
||||||
{ value: '', label: 'Todas as origens' },
|
|
||||||
{ value: 'Geral', label: 'Geral' },
|
|
||||||
{ value: 'Troca de número', label: 'Troca de número' },
|
|
||||||
{ value: 'Chips Virgens e Recebidos', label: 'Chips Virgens e Recebidos' },
|
|
||||||
];
|
|
||||||
|
|
||||||
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<string, EventSummary>();
|
|
||||||
private readonly idFieldExceptions = new Set<string>(['iccid']);
|
|
||||||
|
|
||||||
constructor(
|
|
||||||
private readonly historicoService: HistoricoService,
|
|
||||||
private readonly cdr: ChangeDetectorRef,
|
|
||||||
@Inject(PLATFORM_ID) private readonly platformId: object,
|
|
||||||
private readonly tableExportService: TableExportService
|
|
||||||
) {}
|
|
||||||
|
|
||||||
ngOnInit(): void {
|
|
||||||
this.fetch();
|
|
||||||
}
|
|
||||||
|
|
||||||
applyFilters(): void {
|
|
||||||
this.page = 1;
|
|
||||||
this.fetch();
|
|
||||||
}
|
|
||||||
|
|
||||||
refresh(): void {
|
|
||||||
this.fetch();
|
|
||||||
}
|
|
||||||
|
|
||||||
clearFilters(): void {
|
|
||||||
this.filterChip = '';
|
|
||||||
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();
|
|
||||||
this.fetch();
|
|
||||||
}
|
|
||||||
|
|
||||||
onPageSizeChange(): void {
|
|
||||||
this.page = 1;
|
|
||||||
this.fetch();
|
|
||||||
}
|
|
||||||
|
|
||||||
goToPage(target: number): void {
|
|
||||||
this.page = clampPage(target, this.totalPages);
|
|
||||||
this.fetch();
|
|
||||||
}
|
|
||||||
|
|
||||||
toggleDetails(log: AuditLogDto, event?: Event): void {
|
|
||||||
if (event) event.stopPropagation();
|
|
||||||
this.expandedLogId = this.expandedLogId === log.id ? null : log.id;
|
|
||||||
}
|
|
||||||
|
|
||||||
async onExport(): Promise<void> {
|
|
||||||
if (this.exporting) 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<AuditLogDto>({
|
|
||||||
fileName: `historico_chips_${timestamp}`,
|
|
||||||
sheetName: 'HistoricoChips',
|
|
||||||
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: '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 chips.');
|
|
||||||
} 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 normalizedChipTerm(): string {
|
|
||||||
return (this.filterChip || '').trim();
|
|
||||||
}
|
|
||||||
|
|
||||||
get hasChipFilter(): boolean {
|
|
||||||
return !!this.normalizedChipTerm;
|
|
||||||
}
|
|
||||||
|
|
||||||
get totalPages(): number {
|
|
||||||
return computeTotalPages(this.total || 0, this.pageSize);
|
|
||||||
}
|
|
||||||
|
|
||||||
get pageNumbers(): number[] {
|
|
||||||
return buildPageNumbers(this.page, this.totalPages);
|
|
||||||
}
|
|
||||||
|
|
||||||
get pageStart(): number {
|
|
||||||
return computePageStart(this.total || 0, this.page, this.pageSize);
|
|
||||||
}
|
|
||||||
|
|
||||||
get pageEnd(): number {
|
|
||||||
return computePageEnd(this.total || 0, this.page, this.pageSize);
|
|
||||||
}
|
|
||||||
|
|
||||||
get chipCountInPage(): number {
|
|
||||||
return this.logs.filter((log) => this.summaryFor(log).tone === 'chip').length;
|
|
||||||
}
|
|
||||||
|
|
||||||
get trocaCountInPage(): number {
|
|
||||||
return this.logs.filter((log) => this.summaryFor(log).tone === 'troca').length;
|
|
||||||
}
|
|
||||||
|
|
||||||
get statusCountInPage(): number {
|
|
||||||
return this.logs.filter((log) => this.summaryFor(log).tone === 'status').length;
|
|
||||||
}
|
|
||||||
|
|
||||||
private fetch(): void {
|
|
||||||
this.loading = true;
|
|
||||||
this.error = false;
|
|
||||||
this.errorMsg = '';
|
|
||||||
this.expandedLogId = null;
|
|
||||||
|
|
||||||
const query: ChipHistoricoQuery = {
|
|
||||||
...this.buildBaseQuery(),
|
|
||||||
chip: this.normalizedChipTerm || undefined,
|
|
||||||
page: this.page,
|
|
||||||
pageSize: this.pageSize,
|
|
||||||
};
|
|
||||||
|
|
||||||
this.historicoService.listByChip(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 === 403) {
|
|
||||||
this.errorMsg = 'Acesso restrito.';
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
this.errorMsg = 'Erro ao carregar histórico do chip. Tente novamente.';
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private async fetchAllLogsForExport(): Promise<AuditLogDto[]> {
|
|
||||||
const pageSize = 500;
|
|
||||||
let page = 1;
|
|
||||||
let expectedTotal = 0;
|
|
||||||
const all: AuditLogDto[] = [];
|
|
||||||
|
|
||||||
while (page <= 500) {
|
|
||||||
const response = await firstValueFrom(
|
|
||||||
this.historicoService.listByChip({
|
|
||||||
...this.buildBaseQuery(),
|
|
||||||
chip: this.normalizedChipTerm || undefined,
|
|
||||||
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<ChipHistoricoQuery, 'chip' | 'page' | 'pageSize'> {
|
|
||||||
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', 'numerodochip');
|
|
||||||
const linhaAntiga = this.findChange(log, 'linhaantiga');
|
|
||||||
const linhaNova = this.findChange(log, 'linhanova');
|
|
||||||
|
|
||||||
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 (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',
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
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',
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
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 | undefined>): 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<void> {
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,274 +0,0 @@
|
||||||
<div class="toast-container position-fixed top-0 end-0 p-3" style="z-index: 10000;">
|
|
||||||
<div #successToast class="toast text-bg-danger border-0 shadow" role="alert" aria-live="assertive" aria-atomic="true">
|
|
||||||
<div class="toast-header border-bottom-0">
|
|
||||||
<strong class="me-auto text-primary">LineGestão</strong>
|
|
||||||
<button type="button" class="btn-close" data-bs-dismiss="toast" aria-label="Fechar"></button>
|
|
||||||
</div>
|
|
||||||
<div class="toast-body bg-white rounded-bottom text-dark">
|
|
||||||
{{ toastMessage }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<section class="historico-linhas-page">
|
|
||||||
<span class="page-blob blob-1" aria-hidden="true"></span>
|
|
||||||
<span class="page-blob blob-2" aria-hidden="true"></span>
|
|
||||||
<span class="page-blob blob-3" aria-hidden="true"></span>
|
|
||||||
<span class="page-blob blob-4" aria-hidden="true"></span>
|
|
||||||
|
|
||||||
<div class="container-geral-responsive">
|
|
||||||
<div class="geral-card">
|
|
||||||
<div class="geral-header">
|
|
||||||
<div class="header-row-top">
|
|
||||||
<div class="title-badge">
|
|
||||||
<i class="bi bi-diagram-3-fill"></i> Linha
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="header-title">
|
|
||||||
<h5 class="title mb-0">Histórico de Linhas</h5>
|
|
||||||
<small class="subtitle">Timeline completa das alterações feitas em uma linha específica.</small>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="header-actions d-flex gap-2 justify-content-end">
|
|
||||||
<button type="button" class="btn btn-brand btn-sm" (click)="refresh()" [disabled]="loading">
|
|
||||||
<i class="bi bi-arrow-clockwise me-1"></i> Atualizar
|
|
||||||
</button>
|
|
||||||
<button type="button" class="btn btn-glass btn-sm" (click)="onExport()" [disabled]="loading || exporting">
|
|
||||||
<span *ngIf="!exporting"><i class="bi bi-download me-1"></i> Exportar</span>
|
|
||||||
<span *ngIf="exporting"><span class="spinner-border spinner-border-sm me-2"></span> Exportando...</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="filters-card mt-4">
|
|
||||||
<div class="filters-head">
|
|
||||||
<div class="filters-title">
|
|
||||||
<i class="bi bi-funnel"></i>
|
|
||||||
<span>Filtros</span>
|
|
||||||
</div>
|
|
||||||
<div class="filters-actions">
|
|
||||||
<button class="btn-primary" type="button" (click)="applyFilters()" [disabled]="loading">
|
|
||||||
<i class="bi bi-check2"></i> Aplicar
|
|
||||||
</button>
|
|
||||||
<button class="btn-ghost" type="button" (click)="clearFilters()" [disabled]="loading">
|
|
||||||
<i class="bi bi-x-circle"></i> Limpar
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="filters-grid">
|
|
||||||
<div class="filter-field line-field">
|
|
||||||
<label>Linha</label>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
inputmode="numeric"
|
|
||||||
placeholder="Opcional"
|
|
||||||
[(ngModel)]="filterLine"
|
|
||||||
[disabled]="loading"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="filter-field">
|
|
||||||
<label>Origem</label>
|
|
||||||
<app-select
|
|
||||||
class="select-glass"
|
|
||||||
size="sm"
|
|
||||||
[options]="pageOptions"
|
|
||||||
labelKey="label"
|
|
||||||
valueKey="value"
|
|
||||||
placeholder="Todas"
|
|
||||||
[(ngModel)]="filterPageName"
|
|
||||||
[disabled]="loading">
|
|
||||||
</app-select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="filter-field">
|
|
||||||
<label>Ação</label>
|
|
||||||
<app-select
|
|
||||||
class="select-glass"
|
|
||||||
size="sm"
|
|
||||||
[options]="actionOptions"
|
|
||||||
labelKey="label"
|
|
||||||
valueKey="value"
|
|
||||||
placeholder="Todas"
|
|
||||||
[(ngModel)]="filterAction"
|
|
||||||
[disabled]="loading">
|
|
||||||
</app-select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="filter-field">
|
|
||||||
<label>Usuário</label>
|
|
||||||
<input type="text" placeholder="Nome ou e-mail" [(ngModel)]="filterUser" [disabled]="loading" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="filter-field period-field">
|
|
||||||
<label>Período (De)</label>
|
|
||||||
<input type="date" [(ngModel)]="dateFrom" [disabled]="loading" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="filter-field period-field">
|
|
||||||
<label>Período (Até)</label>
|
|
||||||
<input type="date" [(ngModel)]="dateTo" [disabled]="loading" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="kpi-grid mt-3" *ngIf="logs.length > 0">
|
|
||||||
<div class="kpi-card">
|
|
||||||
<span class="kpi-label">Eventos (filtro)</span>
|
|
||||||
<strong class="kpi-value">{{ total }}</strong>
|
|
||||||
</div>
|
|
||||||
<div class="kpi-card">
|
|
||||||
<span class="kpi-label">Status (página)</span>
|
|
||||||
<strong class="kpi-value">{{ statusCountInPage }}</strong>
|
|
||||||
</div>
|
|
||||||
<div class="kpi-card">
|
|
||||||
<span class="kpi-label">Trocas de Número (página)</span>
|
|
||||||
<strong class="kpi-value">{{ trocaCountInPage }}</strong>
|
|
||||||
</div>
|
|
||||||
<div class="kpi-card">
|
|
||||||
<span class="kpi-label">Mureg (página)</span>
|
|
||||||
<strong class="kpi-value">{{ muregCountInPage }}</strong>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="geral-body">
|
|
||||||
<div class="table-wrap">
|
|
||||||
<div class="text-center p-5" *ngIf="loading">
|
|
||||||
<span class="spinner-border text-brand"></span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="alert alert-danger m-4" role="alert" *ngIf="!loading && error">
|
|
||||||
{{ errorMsg || 'Erro ao carregar histórico da linha.' }}
|
|
||||||
<button class="btn btn-sm btn-outline-danger ms-3" type="button" (click)="refresh()">Tentar novamente</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="empty-group" *ngIf="!loading && !error && logs.length === 0">
|
|
||||||
Nenhuma alteração encontrada para os filtros informados.
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<table class="table table-modern align-middle mb-0" *ngIf="!loading && !error && logs.length > 0">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>Data/Hora</th>
|
|
||||||
<th>Usuário</th>
|
|
||||||
<th>Origem</th>
|
|
||||||
<th>Ação</th>
|
|
||||||
<th>Resumo da alteração</th>
|
|
||||||
<th class="actions-col">Detalhes</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
<ng-container *ngFor="let log of logs; trackBy: trackByLog">
|
|
||||||
<tr class="table-row-item" [class.expanded]="expandedLogId === log.id">
|
|
||||||
<td class="fw-bold text-muted">{{ formatDateTime(log.occurredAtUtc) }}</td>
|
|
||||||
<td>
|
|
||||||
<div class="user-cell">
|
|
||||||
<span class="user-name">{{ displayUserName(log) }}</span>
|
|
||||||
<small class="user-email">{{ log.userEmail || '-' }}</small>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
<span class="origin-pill">{{ log.page || '-' }}</span>
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
<span class="badge-action" [ngClass]="actionClass(log.action)">{{ formatAction(log.action) }}</span>
|
|
||||||
</td>
|
|
||||||
<td class="summary-col">
|
|
||||||
<ng-container *ngIf="summaryFor(log) as summary">
|
|
||||||
<div class="summary-title" [ngClass]="toneClass(summary.tone)">{{ summary.title }}</div>
|
|
||||||
<div class="summary-description">{{ summary.description }}</div>
|
|
||||||
<div class="summary-diff" *ngIf="summary.before || summary.after">
|
|
||||||
<span class="old">{{ formatChangeValue(summary.before) }}</span>
|
|
||||||
<i class="bi bi-arrow-right"></i>
|
|
||||||
<span class="new">{{ formatChangeValue(summary.after) }}</span>
|
|
||||||
</div>
|
|
||||||
<div class="summary-ddd" *ngIf="summary.beforeDdd || summary.afterDdd">
|
|
||||||
DDD: {{ formatChangeValue(summary.beforeDdd) }} <i class="bi bi-arrow-right"></i> {{ formatChangeValue(summary.afterDdd) }}
|
|
||||||
</div>
|
|
||||||
</ng-container>
|
|
||||||
</td>
|
|
||||||
<td class="actions-col">
|
|
||||||
<button
|
|
||||||
class="expand-btn"
|
|
||||||
type="button"
|
|
||||||
(click)="toggleDetails(log, $event)"
|
|
||||||
[attr.aria-expanded]="expandedLogId === log.id"
|
|
||||||
[attr.aria-label]="expandedLogId === log.id ? 'Fechar detalhes' : 'Abrir detalhes'">
|
|
||||||
<i class="bi" [class.bi-chevron-down]="expandedLogId !== log.id" [class.bi-chevron-up]="expandedLogId === log.id"></i>
|
|
||||||
</button>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<tr class="details-row" *ngIf="expandedLogId === log.id">
|
|
||||||
<td colspan="6">
|
|
||||||
<div class="details-panel">
|
|
||||||
<div class="details-section">
|
|
||||||
<div class="section-title">
|
|
||||||
<i class="bi bi-pencil-square"></i> Mudanças de campos
|
|
||||||
</div>
|
|
||||||
<ng-container *ngIf="visibleChanges(log) as changes">
|
|
||||||
<div class="changes-list" *ngIf="changes.length; else noChanges">
|
|
||||||
<div class="change-item" *ngFor="let change of changes; trackBy: trackByField">
|
|
||||||
<div class="change-head">
|
|
||||||
<span class="change-field">{{ change.field }}</span>
|
|
||||||
<span class="change-type" [ngClass]="changeTypeClass(change.changeType)">
|
|
||||||
{{ changeTypeLabel(change.changeType) }}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div class="change-values">
|
|
||||||
<span class="old">{{ formatChangeValue(change.oldValue) }}</span>
|
|
||||||
<i class="bi bi-arrow-right"></i>
|
|
||||||
<span class="new">{{ formatChangeValue(change.newValue) }}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</ng-container>
|
|
||||||
<ng-template #noChanges>
|
|
||||||
<div class="empty-state">Sem mudanças detalhadas nesse evento.</div>
|
|
||||||
</ng-template>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</ng-container>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="geral-footer">
|
|
||||||
<div class="footer-meta">
|
|
||||||
<div class="small text-muted fw-bold">Mostrando {{ pageStart }}–{{ pageEnd }} de {{ total }} registros</div>
|
|
||||||
<div class="page-size d-flex align-items-center gap-2">
|
|
||||||
<span class="text-muted small fw-bold text-uppercase" style="letter-spacing: 0.5px; font-size: 0.75rem;">Itens por pág:</span>
|
|
||||||
<div class="select-wrapper">
|
|
||||||
<app-select
|
|
||||||
class="select-glass"
|
|
||||||
size="sm"
|
|
||||||
[options]="pageSizeOptions"
|
|
||||||
[(ngModel)]="pageSize"
|
|
||||||
(ngModelChange)="onPageSizeChange()"
|
|
||||||
[disabled]="loading">
|
|
||||||
</app-select>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<nav>
|
|
||||||
<ul class="pagination pagination-sm mb-0 pagination-modern">
|
|
||||||
<li class="page-item" [class.disabled]="page === 1 || loading">
|
|
||||||
<button class="page-link" (click)="goToPage(page - 1)">Anterior</button>
|
|
||||||
</li>
|
|
||||||
<li class="page-item" *ngFor="let p of pageNumbers" [class.active]="p === page">
|
|
||||||
<button class="page-link" (click)="goToPage(p)">{{ p }}</button>
|
|
||||||
</li>
|
|
||||||
<li class="page-item" [class.disabled]="page === totalPages || loading">
|
|
||||||
<button class="page-link" (click)="goToPage(page + 1)">Próxima</button>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</nav>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
@ -1,648 +0,0 @@
|
||||||
: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));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,573 +0,0 @@
|
||||||
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';
|
|
||||||
import {
|
|
||||||
buildPageNumbers,
|
|
||||||
clampPage,
|
|
||||||
computePageEnd,
|
|
||||||
computePageStart,
|
|
||||||
computeTotalPages
|
|
||||||
} from '../../utils/pagination.util';
|
|
||||||
|
|
||||||
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<string, EventSummary>();
|
|
||||||
private readonly idFieldExceptions = new Set<string>(['iccid']);
|
|
||||||
|
|
||||||
constructor(
|
|
||||||
private readonly historicoService: HistoricoService,
|
|
||||||
private readonly cdr: ChangeDetectorRef,
|
|
||||||
@Inject(PLATFORM_ID) private readonly platformId: object,
|
|
||||||
private readonly tableExportService: TableExportService
|
|
||||||
) {}
|
|
||||||
|
|
||||||
ngOnInit(): void {
|
|
||||||
this.fetch();
|
|
||||||
}
|
|
||||||
|
|
||||||
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();
|
|
||||||
this.fetch();
|
|
||||||
}
|
|
||||||
|
|
||||||
onPageSizeChange(): void {
|
|
||||||
this.page = 1;
|
|
||||||
this.fetch();
|
|
||||||
}
|
|
||||||
|
|
||||||
goToPage(target: number): void {
|
|
||||||
this.page = clampPage(target, this.totalPages);
|
|
||||||
this.fetch();
|
|
||||||
}
|
|
||||||
|
|
||||||
toggleDetails(log: AuditLogDto, event?: Event): void {
|
|
||||||
if (event) event.stopPropagation();
|
|
||||||
this.expandedLogId = this.expandedLogId === log.id ? null : log.id;
|
|
||||||
}
|
|
||||||
|
|
||||||
async onExport(): Promise<void> {
|
|
||||||
if (this.exporting) 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<AuditLogDto>({
|
|
||||||
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 computeTotalPages(this.total || 0, this.pageSize);
|
|
||||||
}
|
|
||||||
|
|
||||||
get pageNumbers(): number[] {
|
|
||||||
return buildPageNumbers(this.page, this.totalPages);
|
|
||||||
}
|
|
||||||
|
|
||||||
get pageStart(): number {
|
|
||||||
return computePageStart(this.total || 0, this.page, this.pageSize);
|
|
||||||
}
|
|
||||||
|
|
||||||
get pageEnd(): number {
|
|
||||||
return computePageEnd(this.total || 0, this.page, this.pageSize);
|
|
||||||
}
|
|
||||||
|
|
||||||
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 {
|
|
||||||
this.loading = true;
|
|
||||||
this.error = false;
|
|
||||||
this.errorMsg = '';
|
|
||||||
this.expandedLogId = null;
|
|
||||||
|
|
||||||
const query: LineHistoricoQuery = {
|
|
||||||
...this.buildBaseQuery(),
|
|
||||||
line: this.normalizedLineTerm || undefined,
|
|
||||||
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 === 403) {
|
|
||||||
this.errorMsg = 'Acesso restrito.';
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
this.errorMsg = 'Erro ao carregar histórico da linha. Tente novamente.';
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private async fetchAllLogsForExport(): Promise<AuditLogDto[]> {
|
|
||||||
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: this.normalizedLineTerm || undefined,
|
|
||||||
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<LineHistoricoQuery, 'line' | 'page' | 'pageSize'> {
|
|
||||||
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 | undefined>): 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<void> {
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -33,10 +33,6 @@
|
||||||
<button type="button" class="btn btn-brand btn-sm" (click)="refresh()" [disabled]="loading">
|
<button type="button" class="btn btn-brand btn-sm" (click)="refresh()" [disabled]="loading">
|
||||||
<i class="bi bi-arrow-clockwise me-1"></i> Atualizar
|
<i class="bi bi-arrow-clockwise me-1"></i> Atualizar
|
||||||
</button>
|
</button>
|
||||||
<button type="button" class="btn btn-glass btn-sm" (click)="onExport()" [disabled]="loading || exporting">
|
|
||||||
<span *ngIf="!exporting"><i class="bi bi-download me-1"></i> Exportar</span>
|
|
||||||
<span *ngIf="exporting"><span class="spinner-border spinner-border-sm me-2"></span> Exportando...</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,18 +2,9 @@ import { Component, OnInit, ElementRef, ViewChild, ChangeDetectorRef, Inject, PL
|
||||||
import { CommonModule, isPlatformBrowser } from '@angular/common';
|
import { CommonModule, isPlatformBrowser } from '@angular/common';
|
||||||
import { FormsModule } from '@angular/forms';
|
import { FormsModule } from '@angular/forms';
|
||||||
import { HttpErrorResponse } from '@angular/common/http';
|
import { HttpErrorResponse } from '@angular/common/http';
|
||||||
import { firstValueFrom } from 'rxjs';
|
|
||||||
|
|
||||||
import { CustomSelectComponent } from '../../components/custom-select/custom-select';
|
import { CustomSelectComponent } from '../../components/custom-select/custom-select';
|
||||||
import { HistoricoService, AuditLogDto, AuditChangeType, HistoricoQuery } from '../../services/historico.service';
|
import { HistoricoService, AuditLogDto, AuditChangeType, HistoricoQuery } from '../../services/historico.service';
|
||||||
import { TableExportService } from '../../services/table-export.service';
|
|
||||||
import {
|
|
||||||
buildPageNumbers,
|
|
||||||
clampPage,
|
|
||||||
computePageEnd,
|
|
||||||
computePageStart,
|
|
||||||
computeTotalPages
|
|
||||||
} from '../../utils/pagination.util';
|
|
||||||
|
|
||||||
interface SelectOption {
|
interface SelectOption {
|
||||||
value: string;
|
value: string;
|
||||||
|
|
@ -32,7 +23,6 @@ export class Historico implements OnInit {
|
||||||
|
|
||||||
logs: AuditLogDto[] = [];
|
logs: AuditLogDto[] = [];
|
||||||
loading = false;
|
loading = false;
|
||||||
exporting = false;
|
|
||||||
error = false;
|
error = false;
|
||||||
errorMsg = '';
|
errorMsg = '';
|
||||||
toastMessage = '';
|
toastMessage = '';
|
||||||
|
|
@ -75,8 +65,7 @@ export class Historico implements OnInit {
|
||||||
constructor(
|
constructor(
|
||||||
private historicoService: HistoricoService,
|
private historicoService: HistoricoService,
|
||||||
private cdr: ChangeDetectorRef,
|
private cdr: ChangeDetectorRef,
|
||||||
@Inject(PLATFORM_ID) private platformId: object,
|
@Inject(PLATFORM_ID) private platformId: object
|
||||||
private tableExportService: TableExportService
|
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
|
|
@ -122,66 +111,35 @@ export class Historico implements OnInit {
|
||||||
this.fetch();
|
this.fetch();
|
||||||
}
|
}
|
||||||
|
|
||||||
async onExport(): Promise<void> {
|
|
||||||
if (this.exporting) return;
|
|
||||||
this.exporting = true;
|
|
||||||
|
|
||||||
try {
|
|
||||||
const logs = await this.fetchAllLogsForExport();
|
|
||||||
if (!logs.length) {
|
|
||||||
await this.showToast('Nenhum registro encontrado para exportar.');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const timestamp = this.tableExportService.buildTimestamp();
|
|
||||||
await this.tableExportService.exportAsXlsx<AuditLogDto>({
|
|
||||||
fileName: `historico_${timestamp}`,
|
|
||||||
sheetName: 'Historico',
|
|
||||||
rows: logs,
|
|
||||||
columns: [
|
|
||||||
{ header: 'ID', value: (log) => log.id ?? '' },
|
|
||||||
{ header: 'Data/Hora', type: 'datetime', value: (log) => log.occurredAtUtc ?? '' },
|
|
||||||
{ header: 'Usuario', value: (log) => this.displayUserName(log) },
|
|
||||||
{ header: 'E-mail', value: (log) => log.userEmail ?? '' },
|
|
||||||
{ header: 'Pagina', value: (log) => log.page ?? '' },
|
|
||||||
{ header: 'Acao', value: (log) => this.formatAction(log.action) },
|
|
||||||
{ header: 'Entidade', value: (log) => this.displayEntity(log) },
|
|
||||||
{ header: 'Id Entidade', value: (log) => log.entityId ?? '' },
|
|
||||||
{ header: 'Metodo HTTP', value: (log) => log.requestMethod ?? '' },
|
|
||||||
{ header: 'Endpoint', value: (log) => log.requestPath ?? '' },
|
|
||||||
{ header: 'IP', value: (log) => log.ipAddress ?? '' },
|
|
||||||
{ header: 'Mudancas', value: (log) => this.formatChangesSummary(log) },
|
|
||||||
{ header: 'Qtd Mudancas', type: 'number', value: (log) => log.changes?.length ?? 0 },
|
|
||||||
],
|
|
||||||
});
|
|
||||||
|
|
||||||
await this.showToast(`Planilha exportada com ${logs.length} registro(s).`);
|
|
||||||
} catch {
|
|
||||||
await this.showToast('Erro ao exportar planilha.');
|
|
||||||
} finally {
|
|
||||||
this.exporting = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
goToPage(p: number): void {
|
goToPage(p: number): void {
|
||||||
this.page = clampPage(p, this.totalPages);
|
this.page = Math.max(1, Math.min(this.totalPages, p));
|
||||||
this.fetch();
|
this.fetch();
|
||||||
}
|
}
|
||||||
|
|
||||||
get totalPages(): number {
|
get totalPages(): number {
|
||||||
return computeTotalPages(this.total || 0, this.pageSize);
|
return Math.ceil((this.total || 0) / this.pageSize) || 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
get pageNumbers(): number[] {
|
get pageNumbers(): number[] {
|
||||||
return buildPageNumbers(this.page, this.totalPages);
|
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++) pages.push(i);
|
||||||
|
return pages;
|
||||||
}
|
}
|
||||||
|
|
||||||
get pageStart(): number {
|
get pageStart(): number {
|
||||||
return computePageStart(this.total || 0, this.page, this.pageSize);
|
return this.total === 0 ? 0 : (this.page - 1) * this.pageSize + 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
get pageEnd(): number {
|
get pageEnd(): number {
|
||||||
return computePageEnd(this.total || 0, this.page, this.pageSize);
|
if (this.total === 0) return 0;
|
||||||
|
return Math.min(this.page * this.pageSize, this.total);
|
||||||
}
|
}
|
||||||
|
|
||||||
toggleDetails(log: AuditLogDto, event?: Event): void {
|
toggleDetails(log: AuditLogDto, event?: Event): void {
|
||||||
|
|
@ -259,9 +217,14 @@ export class Historico implements OnInit {
|
||||||
this.expandedLogId = null;
|
this.expandedLogId = null;
|
||||||
|
|
||||||
const query: HistoricoQuery = {
|
const query: HistoricoQuery = {
|
||||||
...this.buildBaseQuery(),
|
|
||||||
page: this.page,
|
page: this.page,
|
||||||
pageSize: this.pageSize,
|
pageSize: this.pageSize,
|
||||||
|
pageName: this.filterPageName || undefined,
|
||||||
|
action: this.filterAction || undefined,
|
||||||
|
user: this.filterUser?.trim() || undefined,
|
||||||
|
search: this.filterSearch?.trim() || undefined,
|
||||||
|
dateFrom: this.toIsoDate(this.dateFrom, false) || undefined,
|
||||||
|
dateTo: this.toIsoDate(this.dateTo, true) || undefined,
|
||||||
};
|
};
|
||||||
|
|
||||||
this.historicoService.list(query).subscribe({
|
this.historicoService.list(query).subscribe({
|
||||||
|
|
@ -284,58 +247,6 @@ export class Historico implements OnInit {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private async fetchAllLogsForExport(): Promise<AuditLogDto[]> {
|
|
||||||
const pageSize = 500;
|
|
||||||
let page = 1;
|
|
||||||
let expectedTotal = 0;
|
|
||||||
const all: AuditLogDto[] = [];
|
|
||||||
|
|
||||||
while (page <= 500) {
|
|
||||||
const response = await firstValueFrom(
|
|
||||||
this.historicoService.list({
|
|
||||||
...this.buildBaseQuery(),
|
|
||||||
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<HistoricoQuery, 'page' | 'pageSize'> {
|
|
||||||
return {
|
|
||||||
pageName: this.filterPageName || undefined,
|
|
||||||
action: this.filterAction || undefined,
|
|
||||||
user: this.filterUser?.trim() || undefined,
|
|
||||||
search: this.filterSearch?.trim() || undefined,
|
|
||||||
dateFrom: this.toIsoDate(this.dateFrom, false) || undefined,
|
|
||||||
dateTo: this.toIsoDate(this.dateTo, true) || undefined,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
private formatChangesSummary(log: AuditLogDto): string {
|
|
||||||
const changes = log?.changes ?? [];
|
|
||||||
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 toIsoDate(value: string, endOfDay: boolean): string | null {
|
private toIsoDate(value: string, endOfDay: boolean): string | null {
|
||||||
if (!value) return null;
|
if (!value) return null;
|
||||||
const time = endOfDay ? '23:59:59' : '00:00:00';
|
const time = endOfDay ? '23:59:59' : '00:00:00';
|
||||||
|
|
|
||||||
|
|
@ -31,11 +31,7 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="header-actions d-flex gap-2 justify-content-end" data-animate>
|
<div class="header-actions d-flex gap-2 justify-content-end" data-animate>
|
||||||
<button type="button" class="btn btn-glass btn-sm" (click)="onExport()" [disabled]="loading || exporting">
|
<button type="button" class="btn btn-brand btn-sm" (click)="onCreate()" [disabled]="loading">
|
||||||
<span *ngIf="!exporting"><i class="bi bi-download me-1"></i> Exportar</span>
|
|
||||||
<span *ngIf="exporting"><span class="spinner-border spinner-border-sm me-2"></span> Exportando...</span>
|
|
||||||
</button>
|
|
||||||
<button *ngIf="canManageRecords" type="button" class="btn btn-brand btn-sm" (click)="onCreate()" [disabled]="loading">
|
|
||||||
<i class="bi bi-plus-circle me-1"></i> Nova Mureg
|
<i class="bi bi-plus-circle me-1"></i> Nova Mureg
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -177,10 +173,10 @@
|
||||||
<button class="btn-icon info" (click)="onView(r)" title="Ver Detalhes">
|
<button class="btn-icon info" (click)="onView(r)" title="Ver Detalhes">
|
||||||
<i class="bi bi-eye"></i>
|
<i class="bi bi-eye"></i>
|
||||||
</button>
|
</button>
|
||||||
<button *ngIf="canManageRecords" class="btn-icon primary" (click)="onEditar(r)" title="Editar Registro">
|
<button class="btn-icon primary" (click)="onEditar(r)" title="Editar Registro">
|
||||||
<i class="bi bi-pencil-square"></i>
|
<i class="bi bi-pencil-square"></i>
|
||||||
</button>
|
</button>
|
||||||
<button *ngIf="canManageRecords" class="btn-icon danger" (click)="onDelete(r)" title="Excluir Registro">
|
<button class="btn-icon danger" (click)="onDelete(r)" title="Excluir Registro">
|
||||||
<i class="bi bi-trash"></i>
|
<i class="bi bi-trash"></i>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -222,4 +218,316 @@
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<app-mureg-modals [vm]="$any(vm)"></app-mureg-modals>
|
<div class="modal-backdrop-custom" *ngIf="editOpen || createOpen || deleteOpen || detailOpen" (click)="closeEdit(); closeCreate(); closeDelete(); closeDetail()"></div>
|
||||||
|
|
||||||
|
<!-- ============================== -->
|
||||||
|
<!-- EDIT MODAL -->
|
||||||
|
<!-- ============================== -->
|
||||||
|
<div class="modal-custom" *ngIf="editOpen">
|
||||||
|
<div class="modal-card modal-lg" (click)="$event.stopPropagation()">
|
||||||
|
|
||||||
|
<div class="modal-header">
|
||||||
|
<div class="modal-title">
|
||||||
|
<span class="icon-bg primary-soft"><i class="bi bi-pencil-square"></i></span>
|
||||||
|
Editar Registro Mureg
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="d-flex align-items-center gap-2">
|
||||||
|
<button class="btn btn-glass btn-sm" (click)="closeEdit()" [disabled]="editSaving">
|
||||||
|
<i class="bi bi-x-lg me-1"></i> Cancelar
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button class="btn btn-brand btn-sm" (click)="saveEdit()" [disabled]="!editModel || editSaving">
|
||||||
|
<span *ngIf="!editSaving"><i class="bi bi-check2-circle me-1"></i> Salvar</span>
|
||||||
|
<span *ngIf="editSaving"><span class="spinner-border spinner-border-sm me-2"></span> Salvando...</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="modal-body modern-body bg-light-gray">
|
||||||
|
<ng-container *ngIf="editModel; else editLoadingTpl">
|
||||||
|
<div class="details-dashboard">
|
||||||
|
<div class="detail-box w-100">
|
||||||
|
|
||||||
|
<div class="box-header">
|
||||||
|
<span><i class="bi bi-card-text me-2"></i> Informações</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="box-body">
|
||||||
|
<div class="form-grid">
|
||||||
|
|
||||||
|
<!-- Cliente (select) -->
|
||||||
|
<div class="form-field span-2">
|
||||||
|
<label>Cliente (GERAL)</label>
|
||||||
|
<app-select class="form-control" size="sm" [options]="clientOptions" [(ngModel)]="editModel.selectedClient" (ngModelChange)="onEditClientChange()" placeholder="Selecione..."></app-select>
|
||||||
|
|
||||||
|
<small class="text-muted fw-bold" *ngIf="editClientsLoading">
|
||||||
|
<span class="spinner-border spinner-border-sm me-2"></span>Carregando clientes...
|
||||||
|
</small>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Linha Antiga (select da Geral) -->
|
||||||
|
<div class="form-field span-2">
|
||||||
|
<label>Linha Antiga (GERAL)</label>
|
||||||
|
<app-select class="form-control" size="sm" [options]="lineOptionsEdit" labelKey="label" valueKey="id" [(ngModel)]="editModel.mobileLineId" (ngModelChange)="onEditLineChange()" [disabled]="!editModel.selectedClient || editLinesLoading" placeholder="Selecione a linha do cliente..."></app-select>
|
||||||
|
|
||||||
|
<small class="text-muted fw-bold" *ngIf="editLinesLoading">
|
||||||
|
<span class="spinner-border spinner-border-sm me-2"></span>Carregando linhas...
|
||||||
|
</small>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-field">
|
||||||
|
<label>Item</label>
|
||||||
|
<input class="form-control form-control-sm" [(ngModel)]="editModel.item" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-field">
|
||||||
|
<label>Data Mureg</label>
|
||||||
|
<input class="form-control form-control-sm" type="date" [(ngModel)]="editModel.dataDaMureg" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- LinhaAntiga (snapshot) - preenchido automaticamente -->
|
||||||
|
<div class="form-field">
|
||||||
|
<label>Linha Antiga (snapshot)</label>
|
||||||
|
<input class="form-control form-control-sm" [(ngModel)]="editModel.linhaAntiga" readonly />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-field">
|
||||||
|
<label>Linha Nova</label>
|
||||||
|
<input class="form-control form-control-sm fw-bold text-blue" [(ngModel)]="editModel.linhaNova" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ICCID auto do GERAL -->
|
||||||
|
<div class="form-field span-2">
|
||||||
|
<label>ICCID (auto)</label>
|
||||||
|
<input class="form-control form-control-sm font-monospace" [(ngModel)]="editModel.iccid" readonly />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-3" *ngIf="editModel?.clienteInfo">
|
||||||
|
<small class="text-muted fw-bold">
|
||||||
|
<i class="bi bi-info-circle me-1"></i>
|
||||||
|
{{ editModel.clienteInfo }}
|
||||||
|
</small>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</ng-container>
|
||||||
|
|
||||||
|
<ng-template #editLoadingTpl>
|
||||||
|
<div class="p-5 text-center text-muted">
|
||||||
|
<span class="spinner-border me-2"></span> Preparando edição...
|
||||||
|
</div>
|
||||||
|
</ng-template>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ============================== -->
|
||||||
|
<!-- CREATE MODAL -->
|
||||||
|
<!-- ============================== -->
|
||||||
|
<div class="modal-custom" *ngIf="createOpen">
|
||||||
|
<div class="modal-card modal-lg" (click)="$event.stopPropagation()">
|
||||||
|
|
||||||
|
<div class="modal-header">
|
||||||
|
<div class="modal-title">
|
||||||
|
<span class="icon-bg brand-soft"><i class="bi bi-plus-lg"></i></span>
|
||||||
|
Nova Mureg
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="d-flex align-items-center gap-2">
|
||||||
|
<button class="btn btn-glass btn-sm" (click)="closeCreate()" [disabled]="createSaving">
|
||||||
|
<i class="bi bi-x-lg me-1"></i> Cancelar
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button class="btn btn-brand btn-sm" (click)="saveCreate()" [disabled]="createSaving">
|
||||||
|
<span *ngIf="!createSaving"><i class="bi bi-check2-circle me-1"></i> Criar</span>
|
||||||
|
<span *ngIf="createSaving"><span class="spinner-border spinner-border-sm me-2"></span> Salvando...</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="modal-body modern-body bg-light-gray">
|
||||||
|
<div class="details-dashboard">
|
||||||
|
<div class="detail-box w-100">
|
||||||
|
|
||||||
|
<div class="box-header">
|
||||||
|
<span><i class="bi bi-pencil me-2"></i> Preencha os dados</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="box-body">
|
||||||
|
<div class="form-grid">
|
||||||
|
|
||||||
|
<!-- Cliente (select) -->
|
||||||
|
<div class="form-field span-2">
|
||||||
|
<label>Cliente (GERAL) <span class="text-danger">*</span></label>
|
||||||
|
<app-select class="form-control" size="sm" [options]="clientOptions" [(ngModel)]="createModel.selectedClient" (ngModelChange)="onCreateClientChange()" placeholder="Selecione..."></app-select>
|
||||||
|
|
||||||
|
<small class="text-muted fw-bold" *ngIf="createClientsLoading">
|
||||||
|
<span class="spinner-border spinner-border-sm me-2"></span>Carregando clientes...
|
||||||
|
</small>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Linha Antiga (select Geral) -->
|
||||||
|
<div class="form-field span-2">
|
||||||
|
<label>Linha Antiga (GERAL) <span class="text-danger">*</span></label>
|
||||||
|
<app-select class="form-control" size="sm" [options]="lineOptionsCreate" labelKey="label" valueKey="id" [(ngModel)]="createModel.mobileLineId" (ngModelChange)="onCreateLineChange()" [disabled]="!createModel.selectedClient || createLinesLoading" placeholder="Selecione a linha do cliente..."></app-select>
|
||||||
|
|
||||||
|
<small class="text-muted fw-bold" *ngIf="createLinesLoading">
|
||||||
|
<span class="spinner-border spinner-border-sm me-2"></span>Carregando linhas...
|
||||||
|
</small>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-field">
|
||||||
|
<label>Item</label>
|
||||||
|
<input class="form-control form-control-sm" [(ngModel)]="createModel.item" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-field">
|
||||||
|
<label>Data Mureg</label>
|
||||||
|
<input class="form-control form-control-sm" type="date" [(ngModel)]="createModel.dataDaMureg" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- snapshot -->
|
||||||
|
<div class="form-field">
|
||||||
|
<label>Linha Antiga (snapshot)</label>
|
||||||
|
<input class="form-control form-control-sm" [(ngModel)]="createModel.linhaAntiga" readonly />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-field">
|
||||||
|
<label>Linha Nova <span class="text-danger">*</span></label>
|
||||||
|
<input class="form-control form-control-sm fw-bold text-blue" [(ngModel)]="createModel.linhaNova" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ICCID auto do GERAL -->
|
||||||
|
<div class="form-field span-2">
|
||||||
|
<label>ICCID (auto)</label>
|
||||||
|
<input class="form-control form-control-sm font-monospace" [(ngModel)]="createModel.iccid" readonly />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-3" *ngIf="createModel?.clienteInfo">
|
||||||
|
<small class="text-muted fw-bold">
|
||||||
|
<i class="bi bi-info-circle me-1"></i>
|
||||||
|
{{ createModel.clienteInfo }}
|
||||||
|
</small>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ============================== -->
|
||||||
|
<!-- DETAIL MODAL -->
|
||||||
|
<!-- ============================== -->
|
||||||
|
<div class="modal-custom" *ngIf="detailOpen">
|
||||||
|
<div class="modal-card modal-xl-custom" (click)="$event.stopPropagation()">
|
||||||
|
|
||||||
|
<div class="modal-header">
|
||||||
|
<div class="modal-title">
|
||||||
|
<span class="icon-bg primary-soft"><i class="bi bi-eye"></i></span>
|
||||||
|
Detalhes da Mureg
|
||||||
|
</div>
|
||||||
|
<button class="btn btn-sm btn-icon" (click)="closeDetail()"><i class="bi bi-x-lg"></i></button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="modal-body modern-body bg-light-gray">
|
||||||
|
<div class="p-5 text-center text-muted" *ngIf="detailLoading">
|
||||||
|
<span class="spinner-border me-2"></span> Carregando detalhes...
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="details-dashboard" *ngIf="!detailLoading && detailData">
|
||||||
|
<div class="detail-box">
|
||||||
|
<div class="box-header justify-content-center">
|
||||||
|
<span><i class="bi bi-card-text me-2"></i> Informações da Mureg</span>
|
||||||
|
</div>
|
||||||
|
<div class="box-body">
|
||||||
|
<div class="info-grid">
|
||||||
|
<div class="info-item span-2">
|
||||||
|
<span class="lbl">Linha Nova</span>
|
||||||
|
<span class="val text-blue fs-4">{{ detailData.linhaNova || '-' }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="info-item span-2">
|
||||||
|
<span class="lbl">Linha Antiga</span>
|
||||||
|
<span class="val">{{ detailData.linhaAntiga || '-' }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="info-item span-2">
|
||||||
|
<span class="lbl">Cliente</span>
|
||||||
|
<span class="val">{{ detailData.cliente || '-' }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="info-item span-2">
|
||||||
|
<span class="lbl">Usuário</span>
|
||||||
|
<span class="val">{{ detailData.usuario || '-' }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="info-item">
|
||||||
|
<span class="lbl">Item</span>
|
||||||
|
<span class="val">{{ detailData.item || '-' }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="info-item">
|
||||||
|
<span class="lbl">Data Mureg</span>
|
||||||
|
<span class="val">{{ displayValue('dataDaMureg', detailData.dataDaMureg) }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="info-item span-2">
|
||||||
|
<span class="lbl">ICCID</span>
|
||||||
|
<span class="val small-text">{{ detailData.iccid || '-' }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="info-item span-2">
|
||||||
|
<span class="lbl">Skil</span>
|
||||||
|
<span class="val">{{ detailData.skil || '-' }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ============================== -->
|
||||||
|
<!-- DELETE MODAL -->
|
||||||
|
<!-- ============================== -->
|
||||||
|
<div class="modal-custom" *ngIf="deleteOpen">
|
||||||
|
<div class="modal-card modal-sm" (click)="$event.stopPropagation()">
|
||||||
|
|
||||||
|
<div class="modal-header">
|
||||||
|
<div class="modal-title">
|
||||||
|
<span class="icon-bg danger-soft"><i class="bi bi-trash"></i></span>
|
||||||
|
Excluir Mureg
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button class="btn btn-glass btn-sm" (click)="closeDelete()" [disabled]="deleteSaving">
|
||||||
|
<i class="bi bi-x-lg me-1"></i> Cancelar
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="modal-body">
|
||||||
|
<p class="mb-2 fw-bold">Tem certeza que deseja excluir esta Mureg?</p>
|
||||||
|
<div class="text-muted small">
|
||||||
|
<div><strong>Cliente:</strong> {{ deleteTarget?.cliente || '-' }}</div>
|
||||||
|
<div><strong>Linha nova:</strong> {{ deleteTarget?.linhaNova || '-' }}</div>
|
||||||
|
<div><strong>Linha antiga:</strong> {{ deleteTarget?.linhaAntiga || '-' }}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="d-flex justify-content-end gap-2 mt-4">
|
||||||
|
<button class="btn btn-glass btn-sm" (click)="closeDelete()" [disabled]="deleteSaving">
|
||||||
|
Cancelar
|
||||||
|
</button>
|
||||||
|
<button class="btn btn-danger btn-sm" (click)="confirmDelete()" [disabled]="deleteSaving">
|
||||||
|
<span *ngIf="!deleteSaving"><i class="bi bi-trash me-1"></i> Excluir</span>
|
||||||
|
<span *ngIf="deleteSaving"><span class="spinner-border spinner-border-sm me-2"></span> Excluindo...</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -277,6 +277,8 @@
|
||||||
.pagination-modern .page-item.active .page-link { background-color: var(--blue); border-color: var(--blue); color: #fff; }
|
.pagination-modern .page-item.active .page-link { background-color: var(--blue); border-color: var(--blue); color: #fff; }
|
||||||
|
|
||||||
/* MODALS */
|
/* MODALS */
|
||||||
|
.modal-backdrop-custom { position: fixed; inset: 0; background: rgba(0,0,0,0.45); z-index: 9990; backdrop-filter: blur(4px); }
|
||||||
|
.modal-custom { position: fixed; inset: 0; display: flex; align-items: center; justify-content: center; z-index: 9995; padding: 16px; }
|
||||||
.modal-card { background: #ffffff; border: 1px solid rgba(255,255,255,0.8); border-radius: 20px; box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25); overflow: hidden; display: flex; flex-direction: column; animation: modalPop 0.3s cubic-bezier(0.34, 1.56, 0.64, 1); width: min(850px, 100%); max-height: 90vh; }
|
.modal-card { background: #ffffff; border: 1px solid rgba(255,255,255,0.8); border-radius: 20px; box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25); overflow: hidden; display: flex; flex-direction: column; animation: modalPop 0.3s cubic-bezier(0.34, 1.56, 0.64, 1); width: min(850px, 100%); max-height: 90vh; }
|
||||||
.modal-card.modal-xl-custom { width: min(920px, 90vw); max-height: 78vh; }
|
.modal-card.modal-xl-custom { width: min(920px, 90vw); max-height: 78vh; }
|
||||||
.modal-card.modal-sm { width: min(480px, 100%); }
|
.modal-card.modal-sm { width: min(480px, 100%); }
|
||||||
|
|
|
||||||
|
|
@ -10,22 +10,10 @@ import {
|
||||||
import { isPlatformBrowser, CommonModule } from '@angular/common';
|
import { isPlatformBrowser, CommonModule } from '@angular/common';
|
||||||
import { FormsModule } from '@angular/forms';
|
import { FormsModule } from '@angular/forms';
|
||||||
import { HttpClient, HttpParams } from '@angular/common/http';
|
import { HttpClient, HttpParams } from '@angular/common/http';
|
||||||
import { firstValueFrom } from 'rxjs';
|
|
||||||
import { AuthService } from '../../services/auth.service';
|
|
||||||
import { LinesService } from '../../services/lines.service';
|
import { LinesService } from '../../services/lines.service';
|
||||||
import { CustomSelectComponent } from '../../components/custom-select/custom-select';
|
import { CustomSelectComponent } from '../../components/custom-select/custom-select';
|
||||||
import { TableExportService } from '../../services/table-export.service';
|
|
||||||
import { environment } from '../../../environments/environment';
|
import { environment } from '../../../environments/environment';
|
||||||
import { confirmDeletionWithTyping } from '../../utils/destructive-confirmation';
|
import { confirmDeletionWithTyping } from '../../utils/destructive-confirmation';
|
||||||
import { MuregModalsComponent } from '../../components/page-modals/mureg-modals/mureg-modals';
|
|
||||||
import {
|
|
||||||
buildPageNumbers,
|
|
||||||
clampPage,
|
|
||||||
computePageEnd,
|
|
||||||
computePageStart,
|
|
||||||
computeTotalPages
|
|
||||||
} from '../../utils/pagination.util';
|
|
||||||
import { buildApiEndpoint } from '../../utils/api-base.util';
|
|
||||||
|
|
||||||
type MuregKey = 'item' | 'linhaAntiga' | 'linhaNova' | 'iccid' | 'dataDaMureg' | 'cliente';
|
type MuregKey = 'item' | 'linhaAntiga' | 'linhaNova' | 'iccid' | 'dataDaMureg' | 'cliente';
|
||||||
|
|
||||||
|
|
@ -87,28 +75,15 @@ interface MuregDetailDto {
|
||||||
statusNaGeral: string | null;
|
statusNaGeral: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
type MuregExportRow = MuregRow & {
|
|
||||||
usuario?: string | null;
|
|
||||||
skil?: string | null;
|
|
||||||
linhaAtualNaGeral?: string | null;
|
|
||||||
chipNaGeral?: string | null;
|
|
||||||
contaNaGeral?: string | null;
|
|
||||||
statusNaGeral?: string | null;
|
|
||||||
createdAt?: string | null;
|
|
||||||
updatedAt?: string | null;
|
|
||||||
};
|
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
standalone: true,
|
standalone: true,
|
||||||
imports: [CommonModule, FormsModule, CustomSelectComponent, MuregModalsComponent],
|
imports: [CommonModule, FormsModule, CustomSelectComponent],
|
||||||
templateUrl: './mureg.html',
|
templateUrl: './mureg.html',
|
||||||
styleUrls: ['./mureg.scss']
|
styleUrls: ['./mureg.scss']
|
||||||
})
|
})
|
||||||
export class Mureg implements AfterViewInit {
|
export class Mureg implements AfterViewInit {
|
||||||
readonly vm = this;
|
|
||||||
toastMessage = '';
|
toastMessage = '';
|
||||||
loading = false;
|
loading = false;
|
||||||
exporting = false;
|
|
||||||
|
|
||||||
@ViewChild('successToast', { static: false }) successToast!: ElementRef;
|
@ViewChild('successToast', { static: false }) successToast!: ElementRef;
|
||||||
|
|
||||||
|
|
@ -116,12 +91,14 @@ export class Mureg implements AfterViewInit {
|
||||||
@Inject(PLATFORM_ID) private platformId: object,
|
@Inject(PLATFORM_ID) private platformId: object,
|
||||||
private http: HttpClient,
|
private http: HttpClient,
|
||||||
private cdr: ChangeDetectorRef,
|
private cdr: ChangeDetectorRef,
|
||||||
private authService: AuthService,
|
private linesService: LinesService
|
||||||
private linesService: LinesService,
|
|
||||||
private tableExportService: TableExportService
|
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
private readonly apiBase = buildApiEndpoint(environment.apiUrl, 'mureg');
|
private readonly apiBase = (() => {
|
||||||
|
const raw = (environment.apiUrl || '').replace(/\/+$/, '');
|
||||||
|
const apiBase = raw.toLowerCase().endsWith('/api') ? raw : `${raw}/api`;
|
||||||
|
return `${apiBase}/mureg`;
|
||||||
|
})();
|
||||||
|
|
||||||
// ====== DATA ======
|
// ====== DATA ======
|
||||||
clientGroups: ClientGroup[] = [];
|
clientGroups: ClientGroup[] = [];
|
||||||
|
|
@ -185,20 +162,9 @@ export class Mureg implements AfterViewInit {
|
||||||
clienteInfo: ''
|
clienteInfo: ''
|
||||||
};
|
};
|
||||||
|
|
||||||
isSysAdmin = false;
|
|
||||||
isGestor = false;
|
|
||||||
isFinanceiro = false;
|
|
||||||
|
|
||||||
get canManageRecords(): boolean {
|
|
||||||
return this.isSysAdmin || this.isGestor;
|
|
||||||
}
|
|
||||||
|
|
||||||
async ngAfterViewInit() {
|
async ngAfterViewInit() {
|
||||||
if (!isPlatformBrowser(this.platformId)) return;
|
if (!isPlatformBrowser(this.platformId)) return;
|
||||||
this.initAnimations();
|
this.initAnimations();
|
||||||
this.isSysAdmin = this.authService.hasRole('sysadmin');
|
|
||||||
this.isGestor = this.authService.hasRole('gestor');
|
|
||||||
this.isFinanceiro = this.authService.hasRole('financeiro');
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
this.preloadClients(); // ✅ já deixa o select pronto
|
this.preloadClients(); // ✅ já deixa o select pronto
|
||||||
this.refresh();
|
this.refresh();
|
||||||
|
|
@ -218,147 +184,6 @@ export class Mureg implements AfterViewInit {
|
||||||
this.loadForGroups();
|
this.loadForGroups();
|
||||||
}
|
}
|
||||||
|
|
||||||
async onExport(): Promise<void> {
|
|
||||||
if (this.exporting) return;
|
|
||||||
this.exporting = true;
|
|
||||||
|
|
||||||
try {
|
|
||||||
const baseRows = await this.fetchAllRowsForExport();
|
|
||||||
const rows = await this.fetchDetailedRowsForExport(baseRows);
|
|
||||||
if (!rows.length) {
|
|
||||||
await this.showToast('Nenhum registro encontrado para exportar.');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const timestamp = this.tableExportService.buildTimestamp();
|
|
||||||
await this.tableExportService.exportAsXlsx<MuregExportRow>({
|
|
||||||
fileName: `mureg_${timestamp}`,
|
|
||||||
sheetName: 'Mureg',
|
|
||||||
rows,
|
|
||||||
columns: [
|
|
||||||
{ header: 'ID', value: (row) => row.id ?? '' },
|
|
||||||
{ header: 'Cliente', value: (row) => row.cliente },
|
|
||||||
{ header: 'Usuario', value: (row) => row.usuario ?? '' },
|
|
||||||
{ header: 'Skil', value: (row) => row.skil ?? '' },
|
|
||||||
{ header: 'Item', type: 'number', value: (row) => this.toIntOrZero(row.item) },
|
|
||||||
{ header: 'Linha Antiga', value: (row) => row.linhaAntiga },
|
|
||||||
{ header: 'Linha Nova', value: (row) => row.linhaNova },
|
|
||||||
{ header: 'ICCID', value: (row) => row.iccid },
|
|
||||||
{ header: 'Data da Mureg', type: 'date', value: (row) => row.dataDaMureg },
|
|
||||||
{ header: 'Situacao', value: (row) => (this.isTroca(row) ? 'TROCA' : 'SEM TROCA') },
|
|
||||||
{ header: 'Linha ID (Geral)', value: (row) => row.mobileLineId ?? '' },
|
|
||||||
{ header: 'Linha Atual na Geral', value: (row) => row.linhaAtualNaGeral ?? '' },
|
|
||||||
{ header: 'Chip na Geral', value: (row) => row.chipNaGeral ?? '' },
|
|
||||||
{ header: 'Conta na Geral', value: (row) => row.contaNaGeral ?? '' },
|
|
||||||
{ header: 'Status na Geral', value: (row) => row.statusNaGeral ?? '' },
|
|
||||||
{ header: 'Criado Em', type: 'datetime', value: (row) => row.createdAt ?? '' },
|
|
||||||
{ header: 'Atualizado Em', type: 'datetime', value: (row) => row.updatedAt ?? '' },
|
|
||||||
],
|
|
||||||
});
|
|
||||||
|
|
||||||
await this.showToast(`Planilha exportada com ${rows.length} registro(s).`);
|
|
||||||
} catch {
|
|
||||||
await this.showToast('Erro ao exportar planilha.');
|
|
||||||
} finally {
|
|
||||||
this.exporting = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async fetchAllRowsForExport(): Promise<MuregRow[]> {
|
|
||||||
const pageSize = 2000;
|
|
||||||
let page = 1;
|
|
||||||
let expectedTotal = 0;
|
|
||||||
const rows: MuregRow[] = [];
|
|
||||||
|
|
||||||
while (page <= 500) {
|
|
||||||
const params = new HttpParams()
|
|
||||||
.set('page', String(page))
|
|
||||||
.set('pageSize', String(pageSize))
|
|
||||||
.set('search', (this.searchTerm ?? '').trim())
|
|
||||||
.set('sortBy', 'cliente')
|
|
||||||
.set('sortDir', 'asc');
|
|
||||||
|
|
||||||
const response = await firstValueFrom(
|
|
||||||
this.http.get<ApiPagedResult<any> | any[]>(this.apiBase, { params })
|
|
||||||
);
|
|
||||||
|
|
||||||
const items = Array.isArray(response) ? response : (response.items ?? []);
|
|
||||||
const normalized = items.map((item: any, idx: number) => this.normalizeRow(item, rows.length + idx));
|
|
||||||
rows.push(...normalized);
|
|
||||||
expectedTotal = Array.isArray(response) ? 0 : Number(response.total ?? 0);
|
|
||||||
|
|
||||||
if (Array.isArray(response)) break;
|
|
||||||
if (items.length === 0) break;
|
|
||||||
if (items.length < pageSize) break;
|
|
||||||
if (expectedTotal > 0 && rows.length >= expectedTotal) break;
|
|
||||||
page += 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
return rows.sort((a, b) => {
|
|
||||||
const byClient = (a.cliente ?? '').localeCompare(b.cliente ?? '', 'pt-BR', { sensitivity: 'base' });
|
|
||||||
if (byClient !== 0) return byClient;
|
|
||||||
|
|
||||||
const byItem = this.toIntOrZero(a.item) - this.toIntOrZero(b.item);
|
|
||||||
if (byItem !== 0) return byItem;
|
|
||||||
|
|
||||||
return (a.linhaNova ?? '').localeCompare(b.linhaNova ?? '', 'pt-BR', { sensitivity: 'base' });
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private async fetchDetailedRowsForExport(rows: MuregRow[]): Promise<MuregExportRow[]> {
|
|
||||||
if (!rows.length) return [];
|
|
||||||
|
|
||||||
const result: MuregExportRow[] = [];
|
|
||||||
const chunkSize = 10;
|
|
||||||
|
|
||||||
for (let i = 0; i < rows.length; i += chunkSize) {
|
|
||||||
const chunk = rows.slice(i, i + chunkSize);
|
|
||||||
const detailedChunk = await Promise.all(
|
|
||||||
chunk.map(async (row) => {
|
|
||||||
try {
|
|
||||||
const detail = await firstValueFrom(this.http.get<MuregDetailDto>(`${this.apiBase}/${row.id}`));
|
|
||||||
const merged: MuregExportRow = {
|
|
||||||
...row,
|
|
||||||
item: detail.item !== undefined && detail.item !== null ? String(detail.item) : row.item,
|
|
||||||
linhaAntiga: detail.linhaAntiga ?? row.linhaAntiga,
|
|
||||||
linhaNova: detail.linhaNova ?? row.linhaNova,
|
|
||||||
iccid: detail.iccid ?? row.iccid,
|
|
||||||
dataDaMureg: detail.dataDaMureg ?? row.dataDaMureg,
|
|
||||||
cliente: detail.cliente ?? row.cliente,
|
|
||||||
mobileLineId: detail.mobileLineId ?? row.mobileLineId,
|
|
||||||
usuario: detail.usuario ?? null,
|
|
||||||
skil: detail.skil ?? null,
|
|
||||||
linhaAtualNaGeral: detail.linhaAtualNaGeral ?? null,
|
|
||||||
chipNaGeral: detail.chipNaGeral ?? null,
|
|
||||||
contaNaGeral: detail.contaNaGeral ?? null,
|
|
||||||
statusNaGeral: detail.statusNaGeral ?? null,
|
|
||||||
createdAt: this.getRawField(detail, ['createdAt', 'CreatedAt']) ?? this.getRawField(row.raw, ['createdAt', 'CreatedAt']),
|
|
||||||
updatedAt: this.getRawField(detail, ['updatedAt', 'UpdatedAt']) ?? this.getRawField(row.raw, ['updatedAt', 'UpdatedAt']),
|
|
||||||
};
|
|
||||||
|
|
||||||
return merged;
|
|
||||||
} catch {
|
|
||||||
return {
|
|
||||||
...row,
|
|
||||||
usuario: this.getRawField(row.raw, ['usuario', 'Usuario']),
|
|
||||||
skil: this.getRawField(row.raw, ['skil', 'Skil']),
|
|
||||||
linhaAtualNaGeral: this.getRawField(row.raw, ['linhaAtualNaGeral', 'LinhaAtualNaGeral']),
|
|
||||||
chipNaGeral: this.getRawField(row.raw, ['chipNaGeral', 'ChipNaGeral']),
|
|
||||||
contaNaGeral: this.getRawField(row.raw, ['contaNaGeral', 'ContaNaGeral']),
|
|
||||||
statusNaGeral: this.getRawField(row.raw, ['statusNaGeral', 'StatusNaGeral']),
|
|
||||||
createdAt: this.getRawField(row.raw, ['createdAt', 'CreatedAt']),
|
|
||||||
updatedAt: this.getRawField(row.raw, ['updatedAt', 'UpdatedAt']),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
result.push(...detailedChunk);
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
onSearch() {
|
onSearch() {
|
||||||
if (this.searchTimer) clearTimeout(this.searchTimer);
|
if (this.searchTimer) clearTimeout(this.searchTimer);
|
||||||
this.searchTimer = setTimeout(() => {
|
this.searchTimer = setTimeout(() => {
|
||||||
|
|
@ -383,20 +208,29 @@ export class Mureg implements AfterViewInit {
|
||||||
}
|
}
|
||||||
|
|
||||||
goToPage(p: number) {
|
goToPage(p: number) {
|
||||||
this.page = clampPage(p, this.totalPages);
|
this.page = Math.max(1, Math.min(this.totalPages, p));
|
||||||
this.applyPagination();
|
this.applyPagination();
|
||||||
}
|
}
|
||||||
|
|
||||||
get totalPages() { return computeTotalPages(this.total || 0, this.pageSize); }
|
get totalPages() { return Math.ceil((this.total || 0) / this.pageSize) || 1; }
|
||||||
|
|
||||||
get pageNumbers() {
|
get pageNumbers() {
|
||||||
return buildPageNumbers(this.page, this.totalPages);
|
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++) pages.push(i);
|
||||||
|
return pages;
|
||||||
}
|
}
|
||||||
|
|
||||||
get pageStart() { return computePageStart(this.total || 0, this.page, this.pageSize); }
|
get pageStart() { return this.total === 0 ? 0 : (this.page - 1) * this.pageSize + 1; }
|
||||||
|
|
||||||
get pageEnd() {
|
get pageEnd() {
|
||||||
return computePageEnd(this.total || 0, this.page, this.pageSize);
|
if (this.total === 0) return 0;
|
||||||
|
return Math.min(this.page * this.pageSize, this.total);
|
||||||
}
|
}
|
||||||
|
|
||||||
trackById(_: number, row: MuregRow) { return row.id; }
|
trackById(_: number, row: MuregRow) { return row.id; }
|
||||||
|
|
@ -634,11 +468,6 @@ export class Mureg implements AfterViewInit {
|
||||||
// CREATE MODAL
|
// CREATE MODAL
|
||||||
// =======================================================================
|
// =======================================================================
|
||||||
onCreate() {
|
onCreate() {
|
||||||
if (!this.canManageRecords) {
|
|
||||||
this.showToast('Perfil Financeiro possui acesso somente leitura.');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.preloadClients();
|
this.preloadClients();
|
||||||
|
|
||||||
this.createOpen = true;
|
this.createOpen = true;
|
||||||
|
|
@ -651,7 +480,7 @@ export class Mureg implements AfterViewInit {
|
||||||
linhaAntiga: '',
|
linhaAntiga: '',
|
||||||
linhaNova: '',
|
linhaNova: '',
|
||||||
iccid: '',
|
iccid: '',
|
||||||
dataDaMureg: this.nowDateInput(),
|
dataDaMureg: '',
|
||||||
clienteInfo: ''
|
clienteInfo: ''
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -678,11 +507,6 @@ export class Mureg implements AfterViewInit {
|
||||||
}
|
}
|
||||||
|
|
||||||
saveCreate() {
|
saveCreate() {
|
||||||
if (!this.canManageRecords) {
|
|
||||||
this.showToast('Perfil Financeiro possui acesso somente leitura.');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const mobileLineId = String(this.createModel.mobileLineId ?? '').trim();
|
const mobileLineId = String(this.createModel.mobileLineId ?? '').trim();
|
||||||
const linhaNova = String(this.createModel.linhaNova ?? '').trim();
|
const linhaNova = String(this.createModel.linhaNova ?? '').trim();
|
||||||
|
|
||||||
|
|
@ -699,7 +523,7 @@ export class Mureg implements AfterViewInit {
|
||||||
linhaAntiga: (this.createModel.linhaAntiga ?? '') || null,
|
linhaAntiga: (this.createModel.linhaAntiga ?? '') || null,
|
||||||
linhaNova: (this.createModel.linhaNova ?? '') || null,
|
linhaNova: (this.createModel.linhaNova ?? '') || null,
|
||||||
iccid: (this.createModel.iccid ?? '') || null,
|
iccid: (this.createModel.iccid ?? '') || null,
|
||||||
dataDaMureg: new Date().toISOString()
|
dataDaMureg: this.dateInputToIso(this.createModel.dataDaMureg)
|
||||||
};
|
};
|
||||||
|
|
||||||
if (!payload.item || payload.item <= 0) delete payload.item;
|
if (!payload.item || payload.item <= 0) delete payload.item;
|
||||||
|
|
@ -723,11 +547,6 @@ export class Mureg implements AfterViewInit {
|
||||||
// EDIT MODAL
|
// EDIT MODAL
|
||||||
// =======================================================================
|
// =======================================================================
|
||||||
onEditar(r: MuregRow) {
|
onEditar(r: MuregRow) {
|
||||||
if (!this.canManageRecords) {
|
|
||||||
this.showToast('Perfil Financeiro possui acesso somente leitura.');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.preloadClients();
|
this.preloadClients();
|
||||||
|
|
||||||
this.editOpen = true;
|
this.editOpen = true;
|
||||||
|
|
@ -795,11 +614,6 @@ export class Mureg implements AfterViewInit {
|
||||||
}
|
}
|
||||||
|
|
||||||
saveEdit() {
|
saveEdit() {
|
||||||
if (!this.canManageRecords) {
|
|
||||||
this.showToast('Perfil Financeiro possui acesso somente leitura.');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!this.editModel || !this.editModel.id) return;
|
if (!this.editModel || !this.editModel.id) return;
|
||||||
|
|
||||||
const mobileLineId = String(this.editModel.mobileLineId ?? '').trim();
|
const mobileLineId = String(this.editModel.mobileLineId ?? '').trim();
|
||||||
|
|
@ -874,11 +688,6 @@ export class Mureg implements AfterViewInit {
|
||||||
// DELETE MODAL
|
// DELETE MODAL
|
||||||
// =======================================================================
|
// =======================================================================
|
||||||
onDelete(row: MuregRow) {
|
onDelete(row: MuregRow) {
|
||||||
if (!this.canManageRecords) {
|
|
||||||
this.showToast('Perfil Financeiro possui acesso somente leitura.');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.deleteTarget = row;
|
this.deleteTarget = row;
|
||||||
this.deleteOpen = true;
|
this.deleteOpen = true;
|
||||||
this.deleteSaving = false;
|
this.deleteSaving = false;
|
||||||
|
|
@ -891,11 +700,6 @@ export class Mureg implements AfterViewInit {
|
||||||
}
|
}
|
||||||
|
|
||||||
async confirmDelete() {
|
async confirmDelete() {
|
||||||
if (!this.canManageRecords) {
|
|
||||||
await this.showToast('Perfil Financeiro possui acesso somente leitura.');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!this.deleteTarget?.id) return;
|
if (!this.deleteTarget?.id) return;
|
||||||
if (!(await confirmDeletionWithTyping('esta Mureg'))) return;
|
if (!(await confirmDeletionWithTyping('esta Mureg'))) return;
|
||||||
|
|
||||||
|
|
@ -954,14 +758,6 @@ export class Mureg implements AfterViewInit {
|
||||||
return dt.toISOString();
|
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 {
|
private extractApiMessage(err: any): string | null {
|
||||||
try {
|
try {
|
||||||
const m1 = err?.error?.message;
|
const m1 = err?.error?.message;
|
||||||
|
|
@ -974,15 +770,6 @@ export class Mureg implements AfterViewInit {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private getRawField(source: any, keys: string[]): string | null {
|
|
||||||
for (const key of keys) {
|
|
||||||
const value = source?.[key];
|
|
||||||
if (value === undefined || value === null || String(value).trim() === '') continue;
|
|
||||||
return String(value);
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
displayValue(key: MuregKey, v: any): string {
|
displayValue(key: MuregKey, v: any): string {
|
||||||
if (v === null || v === undefined || String(v).trim() === '') return '-';
|
if (v === null || v === undefined || String(v).trim() === '') return '-';
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,331 +0,0 @@
|
||||||
<div class="toast-container position-fixed top-0 end-0 p-3" style="z-index: 10000;">
|
|
||||||
<div #feedbackToast class="toast text-bg-success border-0 shadow" role="alert" aria-live="assertive" aria-atomic="true">
|
|
||||||
<div class="toast-header border-bottom-0">
|
|
||||||
<strong class="me-auto text-primary">LineGestão</strong>
|
|
||||||
<button type="button" class="btn-close" data-bs-dismiss="toast" aria-label="Fechar"></button>
|
|
||||||
</div>
|
|
||||||
<div class="toast-body bg-white rounded-bottom text-dark">
|
|
||||||
{{ toastMessage }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<section class="mve-page">
|
|
||||||
<span class="page-blob blob-1" aria-hidden="true"></span>
|
|
||||||
<span class="page-blob blob-2" aria-hidden="true"></span>
|
|
||||||
<span class="page-blob blob-3" aria-hidden="true"></span>
|
|
||||||
<span class="page-blob blob-4" aria-hidden="true"></span>
|
|
||||||
|
|
||||||
<div class="page-shell">
|
|
||||||
<div class="page-card">
|
|
||||||
<div class="page-header">
|
|
||||||
<div class="header-top">
|
|
||||||
<div class="title-badge">
|
|
||||||
<i class="bi bi-shield-check"></i> Relatorio da Vivo
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="header-title">
|
|
||||||
<h5 class="title mb-0">Auditoria MVE</h5>
|
|
||||||
<small class="subtitle">Compare os status das linhas e atualize o sistema quando precisar.</small>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="header-actions">
|
|
||||||
<button type="button" class="btn btn-glass btn-sm" (click)="loadLatestAudit()" [disabled]="loadingLatest || processing || syncing">
|
|
||||||
<span *ngIf="!loadingLatest"><i class="bi bi-arrow-clockwise me-1"></i> Ultima conferencia</span>
|
|
||||||
<span *ngIf="loadingLatest"><span class="spinner-border spinner-border-sm me-2"></span> Carregando...</span>
|
|
||||||
</button>
|
|
||||||
<button type="button" class="btn btn-brand btn-sm" (click)="syncIssues()" [disabled]="syncing || syncableIssues.length === 0">
|
|
||||||
<span *ngIf="!syncing"><i class="bi bi-arrow-repeat me-1"></i> Atualizar sistema</span>
|
|
||||||
<span *ngIf="syncing"><span class="spinner-border spinner-border-sm me-2"></span> Sincronizando...</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="intro-card">
|
|
||||||
<div>
|
|
||||||
<div class="intro-title">Conferencia</div>
|
|
||||||
<p class="intro-text mb-0">
|
|
||||||
Use o relatorio da Vivo para conferir se <strong>status, linha e chip</strong> estão alinhados com o sistema.
|
|
||||||
Mudanças só de DDD continuam sendo sinalizadas apenas para revisão manual.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="intro-meta" *ngIf="auditResult">
|
|
||||||
<span class="meta-pill"><i class="bi bi-filetype-csv me-1"></i> {{ auditResult.fileName || 'arquivo sem nome' }}</span>
|
|
||||||
<span class="meta-pill">Ultima conferencia: {{ formatDateTime(auditResult.importedAtUtc) }}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="upload-card">
|
|
||||||
<div class="section-head">
|
|
||||||
<div>
|
|
||||||
<div class="section-title">Enviar relatorio</div>
|
|
||||||
<div class="section-subtitle">Selecione o arquivo da Vivo para conferir os status.</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="upload-grid">
|
|
||||||
<label class="upload-zone">
|
|
||||||
<input type="file" accept=".csv,text/csv" (change)="onFileSelected($event)" [disabled]="processing || syncing" />
|
|
||||||
<span class="upload-icon"><i class="bi bi-cloud-arrow-up"></i></span>
|
|
||||||
<span class="upload-title">{{ selectedFile ? selectedFile.name : 'Selecionar relatorio da Vivo' }}</span>
|
|
||||||
<span class="upload-subtitle">Escolha o arquivo exportado pela Vivo.</span>
|
|
||||||
</label>
|
|
||||||
|
|
||||||
<div class="upload-actions">
|
|
||||||
<button type="button" class="btn btn-glass btn-sm" (click)="clearSelectedFile()" [disabled]="!selectedFile || processing">
|
|
||||||
Limpar arquivo
|
|
||||||
</button>
|
|
||||||
<button type="button" class="btn btn-brand btn-sm" (click)="processAudit()" [disabled]="!selectedFile || processing || syncing">
|
|
||||||
<span *ngIf="!processing"><i class="bi bi-play-circle me-1"></i> Conferir relatorio</span>
|
|
||||||
<span *ngIf="processing"><span class="spinner-border spinner-border-sm me-2"></span> Processando...</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="alert alert-danger mb-0 mt-3" *ngIf="errorMessage">
|
|
||||||
<i class="bi bi-exclamation-octagon me-2"></i>{{ errorMessage }}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="apply-banner" *ngIf="applyResult">
|
|
||||||
<strong>Sincronização concluída.</strong>
|
|
||||||
{{ applyResult.updatedLines }} linha(s) atualizada(s).
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="page-body" *ngIf="auditResult as audit; else emptyState">
|
|
||||||
<div class="summary-grid">
|
|
||||||
<article class="summary-card">
|
|
||||||
<span class="summary-label">No sistema</span>
|
|
||||||
<strong>{{ audit.summary.totalSystemLines }}</strong>
|
|
||||||
</article>
|
|
||||||
<article class="summary-card">
|
|
||||||
<span class="summary-label">No relatorio</span>
|
|
||||||
<strong>{{ audit.summary.totalReportLines }}</strong>
|
|
||||||
</article>
|
|
||||||
<article class="summary-card is-positive">
|
|
||||||
<span class="summary-label">Sem diferenca</span>
|
|
||||||
<strong>{{ audit.summary.totalConciliated }}</strong>
|
|
||||||
</article>
|
|
||||||
<article class="summary-card is-danger">
|
|
||||||
<span class="summary-label">Com diferenca</span>
|
|
||||||
<strong>{{ totalDifferencesCount }}</strong>
|
|
||||||
</article>
|
|
||||||
<article class="summary-card is-brand">
|
|
||||||
<span class="summary-label">Prontas para atualizar</span>
|
|
||||||
<strong>{{ syncableIssues.length }}</strong>
|
|
||||||
</article>
|
|
||||||
<article class="summary-card">
|
|
||||||
<span class="summary-label">Revisão manual</span>
|
|
||||||
<strong>{{ manualReviewIssuesCount }}</strong>
|
|
||||||
</article>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="secondary-notes" *ngIf="audit.summary.totalOnlyInSystem > 0 || audit.summary.totalOnlyInReport > 0 || ignoredIssuesCount > 0">
|
|
||||||
<span class="meta-pill" *ngIf="audit.summary.totalOnlyInSystem > 0">Só no sistema: {{ audit.summary.totalOnlyInSystem }}</span>
|
|
||||||
<span class="meta-pill" *ngIf="audit.summary.totalOnlyInReport > 0">Só no relatório: {{ audit.summary.totalOnlyInReport }}</span>
|
|
||||||
<span class="meta-pill" *ngIf="ignoredIssuesCount > 0">Avisos/ignorados: {{ ignoredIssuesCount }}</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="toolbar">
|
|
||||||
<div class="toolbar-left">
|
|
||||||
<div class="view-tabs">
|
|
||||||
<button type="button" class="filter-tab" [class.active]="viewMode === 'PENDING'" (click)="setViewMode('PENDING')">
|
|
||||||
Para atualizar
|
|
||||||
</button>
|
|
||||||
<button type="button" class="filter-tab" [class.active]="viewMode === 'APPLIED'" (click)="setViewMode('APPLIED')">
|
|
||||||
Atualizadas
|
|
||||||
</button>
|
|
||||||
<button type="button" class="filter-tab" [class.active]="viewMode === 'ALL'" (click)="setViewMode('ALL')">
|
|
||||||
Todas
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="type-filters" *ngIf="relevantIssues.length > 0">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class="type-filter is-all"
|
|
||||||
[class.active]="issueCategory === 'ALL'"
|
|
||||||
(click)="setIssueCategory('ALL')">
|
|
||||||
<span>Todas</span>
|
|
||||||
<strong>{{ relevantIssues.length }}</strong>
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class="type-filter is-line"
|
|
||||||
[class.active]="issueCategory === 'LINE'"
|
|
||||||
(click)="setIssueCategory('LINE')">
|
|
||||||
<span>Troca de linha</span>
|
|
||||||
<strong>{{ lineIssuesCount }}</strong>
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class="type-filter is-chip"
|
|
||||||
[class.active]="issueCategory === 'CHIP'"
|
|
||||||
(click)="setIssueCategory('CHIP')">
|
|
||||||
<span>Troca de chip</span>
|
|
||||||
<strong>{{ chipIssuesCount }}</strong>
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class="type-filter is-status"
|
|
||||||
[class.active]="issueCategory === 'STATUS'"
|
|
||||||
(click)="setIssueCategory('STATUS')">
|
|
||||||
<span>Status</span>
|
|
||||||
<strong>{{ statusIssuesCount }}</strong>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="toolbar-right">
|
|
||||||
<div class="input-group input-group-sm search-group">
|
|
||||||
<span class="input-group-text"><i class="bi bi-search"></i></span>
|
|
||||||
<input
|
|
||||||
class="form-control"
|
|
||||||
placeholder="Buscar por linha, chip, status ou situação"
|
|
||||||
[(ngModel)]="searchTerm"
|
|
||||||
(ngModelChange)="onSearchChange()"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<select class="form-select form-select-sm page-size-select" [(ngModel)]="pageSize" (ngModelChange)="onPageSizeChange()">
|
|
||||||
<option *ngFor="let option of pageSizeOptions" [ngValue]="option">{{ option }} por página</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="status-empty" *ngIf="filteredIssues.length === 0">
|
|
||||||
<i class="bi bi-check2-circle"></i>
|
|
||||||
<div>Nenhuma divergência encontrada para o filtro atual.</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="table-wrap" *ngIf="filteredIssues.length > 0">
|
|
||||||
<table class="table table-modern align-middle mb-0">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>Número</th>
|
|
||||||
<th>Sistema</th>
|
|
||||||
<th>Relatório</th>
|
|
||||||
<th>Situação</th>
|
|
||||||
<th>Ação</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
<tr *ngFor="let issue of pagedIssues; trackBy: trackByIssue" [class.row-applied]="issue.applied">
|
|
||||||
<td class="cell-line">
|
|
||||||
<div class="line-cell-stack">
|
|
||||||
<span class="line-number-chip">{{ issue.numeroLinha || issue.reportSnapshot?.numeroLinha || issue.systemSnapshot?.numeroLinha || '-' }}</span>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
<td class="cell-compare">
|
|
||||||
<div class="issue-card issue-card-system">
|
|
||||||
<div class="issue-card-head">
|
|
||||||
<span class="issue-card-eyebrow">Sistema</span>
|
|
||||||
<span class="issue-card-caption">Cadastro atual</span>
|
|
||||||
</div>
|
|
||||||
<div class="issue-row" [class.is-different]="hasDifference(issue, 'line')">
|
|
||||||
<div class="issue-row-head">
|
|
||||||
<span class="issue-label">Linha</span>
|
|
||||||
<span class="field-diff-flag" *ngIf="hasDifference(issue, 'line')">Alterada</span>
|
|
||||||
</div>
|
|
||||||
<span class="issue-value">{{ formatValue(issue.systemSnapshot?.numeroLinha) }}</span>
|
|
||||||
</div>
|
|
||||||
<div class="issue-row" [class.is-different]="hasDifference(issue, 'chip')">
|
|
||||||
<div class="issue-row-head">
|
|
||||||
<span class="issue-label">Chip</span>
|
|
||||||
<span class="field-diff-flag" *ngIf="hasDifference(issue, 'chip')">Alterado</span>
|
|
||||||
</div>
|
|
||||||
<span class="issue-value">{{ formatValue(issue.systemSnapshot?.chip) }}</span>
|
|
||||||
</div>
|
|
||||||
<div class="issue-row" [class.is-different]="hasDifference(issue, 'status')">
|
|
||||||
<div class="issue-row-head">
|
|
||||||
<span class="issue-label">Status</span>
|
|
||||||
<span class="field-diff-flag" *ngIf="hasDifference(issue, 'status')">Divergente</span>
|
|
||||||
</div>
|
|
||||||
<span class="status-pill" [ngClass]="statusClass(issue.systemStatus)">{{ statusLabel(issue.systemStatus) }}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
<td class="cell-compare">
|
|
||||||
<div class="issue-card issue-card-report">
|
|
||||||
<div class="issue-card-head">
|
|
||||||
<span class="issue-card-eyebrow">Relatório</span>
|
|
||||||
<span class="issue-card-caption">Importado do MVE</span>
|
|
||||||
</div>
|
|
||||||
<div class="issue-row" [class.is-different]="hasDifference(issue, 'line')">
|
|
||||||
<div class="issue-row-head">
|
|
||||||
<span class="issue-label">Linha</span>
|
|
||||||
<span class="field-diff-flag" *ngIf="hasDifference(issue, 'line')">Nova</span>
|
|
||||||
</div>
|
|
||||||
<span class="issue-value">{{ formatValue(issue.reportSnapshot?.numeroLinha) }}</span>
|
|
||||||
</div>
|
|
||||||
<div class="issue-row" [class.is-different]="hasDifference(issue, 'chip')">
|
|
||||||
<div class="issue-row-head">
|
|
||||||
<span class="issue-label">Chip</span>
|
|
||||||
<span class="field-diff-flag" *ngIf="hasDifference(issue, 'chip')">Novo</span>
|
|
||||||
</div>
|
|
||||||
<span class="issue-value">{{ formatValue(issue.reportSnapshot?.chip) }}</span>
|
|
||||||
</div>
|
|
||||||
<div class="issue-row" [class.is-different]="hasDifference(issue, 'status')">
|
|
||||||
<div class="issue-row-head">
|
|
||||||
<span class="issue-label">Status</span>
|
|
||||||
<span class="field-diff-flag" *ngIf="hasDifference(issue, 'status')">MVE</span>
|
|
||||||
</div>
|
|
||||||
<span class="status-pill" [ngClass]="statusClass(issue.reportStatus)">{{ statusLabel(issue.reportStatus) }}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
<td class="cell-situation">
|
|
||||||
<div class="situation-card" [ngClass]="situationClass(issue)">
|
|
||||||
<div class="situation-top">
|
|
||||||
<span class="issue-kind-badge" [ngClass]="issueKindClass(issue)">{{ issueKindLabel(issue) }}</span>
|
|
||||||
</div>
|
|
||||||
<div class="difference-tags" *ngIf="issue.differences.length > 0">
|
|
||||||
</div>
|
|
||||||
<div class="issue-notes" *ngIf="issue.notes">{{ issue.notes }}</div>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
<td class="cell-action">
|
|
||||||
<div class="action-card">
|
|
||||||
<span class="sync-badge ready" *ngIf="issue.syncable && !issue.applied">Pode atualizar</span>
|
|
||||||
<span class="sync-badge applied" *ngIf="issue.applied">Atualizada</span>
|
|
||||||
<span class="sync-badge muted" *ngIf="!issue.syncable && !issue.applied">Revisar</span>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="page-footer" *ngIf="filteredIssues.length > 0">
|
|
||||||
<div class="small text-muted fw-bold">
|
|
||||||
Mostrando {{ pageStart }}–{{ pageEnd }} de {{ filteredIssues.length }} divergência(s)
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<nav>
|
|
||||||
<ul class="pagination pagination-sm mb-0 pagination-modern">
|
|
||||||
<li class="page-item" [class.disabled]="page === 1">
|
|
||||||
<button class="page-link" type="button" (click)="goToPage(page - 1)">Anterior</button>
|
|
||||||
</li>
|
|
||||||
<li class="page-item" *ngFor="let p of pageNumbers" [class.active]="p === page">
|
|
||||||
<button class="page-link" type="button" (click)="goToPage(p)">{{ p }}</button>
|
|
||||||
</li>
|
|
||||||
<li class="page-item" [class.disabled]="page === totalPages">
|
|
||||||
<button class="page-link" type="button" (click)="goToPage(page + 1)">Próxima</button>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</nav>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<ng-template #emptyState>
|
|
||||||
<div class="empty-state">
|
|
||||||
<i class="bi bi-file-earmark-spreadsheet"></i>
|
|
||||||
<div>Nenhuma conferencia carregada ainda.</div>
|
|
||||||
<small>Envie o relatorio da Vivo para ver divergências de status, linha e chip antes de atualizar o sistema.</small>
|
|
||||||
</div>
|
|
||||||
</ng-template>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
@ -1,949 +0,0 @@
|
||||||
:host {
|
|
||||||
--brand: #e33dcf;
|
|
||||||
--brand-deep: #972688;
|
|
||||||
--brand-soft: rgba(227, 61, 207, 0.1);
|
|
||||||
--ink: #17161d;
|
|
||||||
--muted: rgba(23, 22, 29, 0.68);
|
|
||||||
--surface: rgba(255, 255, 255, 0.84);
|
|
||||||
--surface-strong: rgba(255, 255, 255, 0.94);
|
|
||||||
--line: rgba(23, 22, 29, 0.08);
|
|
||||||
--shadow: 0 24px 60px rgba(24, 17, 33, 0.12);
|
|
||||||
--success: #198754;
|
|
||||||
--danger: #dc3545;
|
|
||||||
--warning: #ffb200;
|
|
||||||
|
|
||||||
display: block;
|
|
||||||
color: var(--ink);
|
|
||||||
font-family: 'Inter', sans-serif;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mve-page {
|
|
||||||
min-height: 100vh;
|
|
||||||
position: relative;
|
|
||||||
overflow: hidden;
|
|
||||||
padding: 0 14px 120px;
|
|
||||||
background:
|
|
||||||
radial-gradient(720px 380px at 12% 8%, rgba(227, 61, 207, 0.16), transparent 60%),
|
|
||||||
radial-gradient(620px 360px at 88% 12%, rgba(3, 15, 170, 0.08), transparent 58%),
|
|
||||||
linear-gradient(180deg, #fff 0%, #f6f4f8 100%);
|
|
||||||
}
|
|
||||||
|
|
||||||
.page-blob {
|
|
||||||
position: fixed;
|
|
||||||
border-radius: 999px;
|
|
||||||
filter: blur(48px);
|
|
||||||
pointer-events: none;
|
|
||||||
opacity: 0.42;
|
|
||||||
z-index: 0;
|
|
||||||
background: radial-gradient(circle at 30% 30%, rgba(227, 61, 207, 0.6), rgba(227, 61, 207, 0.04));
|
|
||||||
|
|
||||||
&.blob-1 { width: 420px; height: 420px; top: -180px; left: -120px; }
|
|
||||||
&.blob-2 { width: 520px; height: 520px; top: -220px; right: -220px; }
|
|
||||||
&.blob-3 { width: 360px; height: 360px; bottom: -180px; left: 20%; }
|
|
||||||
&.blob-4 { width: 420px; height: 420px; bottom: -200px; right: 12%; }
|
|
||||||
}
|
|
||||||
|
|
||||||
.page-shell {
|
|
||||||
position: relative;
|
|
||||||
z-index: 1;
|
|
||||||
width: min(1480px, 98vw);
|
|
||||||
margin: 38px auto 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.page-card {
|
|
||||||
background: var(--surface);
|
|
||||||
border: 1px solid rgba(255, 255, 255, 0.72);
|
|
||||||
border-radius: 28px;
|
|
||||||
box-shadow: var(--shadow);
|
|
||||||
backdrop-filter: blur(12px);
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.page-header {
|
|
||||||
padding: 22px 24px 18px;
|
|
||||||
border-bottom: 1px solid var(--line);
|
|
||||||
background: linear-gradient(180deg, rgba(255,255,255,0.92), rgba(255,255,255,0.72));
|
|
||||||
display: grid;
|
|
||||||
justify-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.page-header > * {
|
|
||||||
width: min(1120px, 100%);
|
|
||||||
}
|
|
||||||
|
|
||||||
.header-top {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: 1fr;
|
|
||||||
gap: 16px;
|
|
||||||
align-items: center;
|
|
||||||
justify-items: center;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.title-badge {
|
|
||||||
justify-self: center;
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 10px;
|
|
||||||
padding: 8px 14px;
|
|
||||||
border-radius: 999px;
|
|
||||||
background: rgba(255, 255, 255, 0.92);
|
|
||||||
border: 1px solid rgba(227, 61, 207, 0.22);
|
|
||||||
box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.55);
|
|
||||||
font-size: 13px;
|
|
||||||
font-weight: 800;
|
|
||||||
|
|
||||||
i {
|
|
||||||
color: var(--brand);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.header-title {
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.title {
|
|
||||||
font-size: 28px;
|
|
||||||
font-weight: 950;
|
|
||||||
letter-spacing: -0.04em;
|
|
||||||
color: var(--ink);
|
|
||||||
}
|
|
||||||
|
|
||||||
.subtitle {
|
|
||||||
color: var(--muted);
|
|
||||||
font-weight: 700;
|
|
||||||
}
|
|
||||||
|
|
||||||
.header-actions {
|
|
||||||
justify-self: center;
|
|
||||||
display: flex;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
justify-content: center;
|
|
||||||
gap: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn {
|
|
||||||
border-radius: 14px;
|
|
||||||
font-weight: 800;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-glass {
|
|
||||||
background: rgba(255, 255, 255, 0.92);
|
|
||||||
border: 1px solid rgba(23, 22, 29, 0.08);
|
|
||||||
color: var(--ink);
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-brand {
|
|
||||||
background: linear-gradient(135deg, var(--brand), var(--brand-deep));
|
|
||||||
border: 0;
|
|
||||||
color: #fff;
|
|
||||||
box-shadow: 0 12px 24px rgba(151, 38, 136, 0.24);
|
|
||||||
}
|
|
||||||
|
|
||||||
.intro-card,
|
|
||||||
.upload-card,
|
|
||||||
.secondary-notes,
|
|
||||||
.toolbar,
|
|
||||||
.summary-grid,
|
|
||||||
.table-wrap,
|
|
||||||
.status-empty,
|
|
||||||
.empty-state {
|
|
||||||
margin-top: 18px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.intro-card,
|
|
||||||
.upload-card,
|
|
||||||
.toolbar,
|
|
||||||
.table-wrap,
|
|
||||||
.status-empty,
|
|
||||||
.empty-state {
|
|
||||||
background: var(--surface-strong);
|
|
||||||
border: 1px solid var(--line);
|
|
||||||
border-radius: 20px;
|
|
||||||
box-shadow: 0 18px 34px rgba(24, 17, 33, 0.08);
|
|
||||||
}
|
|
||||||
|
|
||||||
.intro-card {
|
|
||||||
padding: 18px 20px;
|
|
||||||
display: grid;
|
|
||||||
gap: 14px;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.intro-title,
|
|
||||||
.section-title {
|
|
||||||
font-size: 14px;
|
|
||||||
font-weight: 900;
|
|
||||||
text-transform: uppercase;
|
|
||||||
letter-spacing: 0.08em;
|
|
||||||
color: var(--brand-deep);
|
|
||||||
}
|
|
||||||
|
|
||||||
.intro-text,
|
|
||||||
.section-subtitle {
|
|
||||||
color: var(--muted);
|
|
||||||
line-height: 1.55;
|
|
||||||
}
|
|
||||||
|
|
||||||
.intro-meta,
|
|
||||||
.secondary-notes {
|
|
||||||
display: flex;
|
|
||||||
gap: 8px;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.meta-pill {
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 6px;
|
|
||||||
padding: 7px 12px;
|
|
||||||
border-radius: 999px;
|
|
||||||
background: rgba(24, 17, 33, 0.05);
|
|
||||||
border: 1px solid rgba(24, 17, 33, 0.08);
|
|
||||||
font-size: 12px;
|
|
||||||
font-weight: 700;
|
|
||||||
color: rgba(24, 17, 33, 0.78);
|
|
||||||
}
|
|
||||||
|
|
||||||
.upload-card {
|
|
||||||
padding: 20px;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.section-head {
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
gap: 12px;
|
|
||||||
align-items: flex-start;
|
|
||||||
}
|
|
||||||
|
|
||||||
.upload-grid {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: minmax(0, 1fr) auto;
|
|
||||||
gap: 16px;
|
|
||||||
margin-top: 16px;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.upload-zone {
|
|
||||||
display: grid;
|
|
||||||
place-items: center;
|
|
||||||
gap: 8px;
|
|
||||||
min-height: 180px;
|
|
||||||
padding: 20px;
|
|
||||||
border-radius: 22px;
|
|
||||||
border: 1px dashed rgba(227, 61, 207, 0.35);
|
|
||||||
background:
|
|
||||||
linear-gradient(135deg, rgba(227, 61, 207, 0.06), rgba(255, 255, 255, 0.85)),
|
|
||||||
#fff;
|
|
||||||
text-align: center;
|
|
||||||
cursor: pointer;
|
|
||||||
|
|
||||||
input {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.upload-icon {
|
|
||||||
width: 62px;
|
|
||||||
height: 62px;
|
|
||||||
display: grid;
|
|
||||||
place-items: center;
|
|
||||||
border-radius: 20px;
|
|
||||||
background: rgba(227, 61, 207, 0.12);
|
|
||||||
color: var(--brand-deep);
|
|
||||||
font-size: 28px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.upload-title {
|
|
||||||
font-size: 18px;
|
|
||||||
font-weight: 900;
|
|
||||||
color: var(--ink);
|
|
||||||
}
|
|
||||||
|
|
||||||
.upload-subtitle {
|
|
||||||
max-width: 540px;
|
|
||||||
color: var(--muted);
|
|
||||||
line-height: 1.45;
|
|
||||||
}
|
|
||||||
|
|
||||||
.upload-actions {
|
|
||||||
display: grid;
|
|
||||||
gap: 10px;
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.apply-banner {
|
|
||||||
margin-top: 14px;
|
|
||||||
padding: 12px 14px;
|
|
||||||
border-radius: 16px;
|
|
||||||
background: rgba(25, 135, 84, 0.1);
|
|
||||||
border: 1px solid rgba(25, 135, 84, 0.18);
|
|
||||||
color: #11653d;
|
|
||||||
font-weight: 700;
|
|
||||||
}
|
|
||||||
|
|
||||||
.page-body {
|
|
||||||
padding: 0 24px 24px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.summary-grid {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(6, minmax(0, 1fr));
|
|
||||||
gap: 14px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.summary-card {
|
|
||||||
background: rgba(255, 255, 255, 0.94);
|
|
||||||
border: 1px solid var(--line);
|
|
||||||
border-radius: 20px;
|
|
||||||
padding: 18px;
|
|
||||||
display: grid;
|
|
||||||
gap: 10px;
|
|
||||||
box-shadow: 0 16px 30px rgba(24, 17, 33, 0.08);
|
|
||||||
|
|
||||||
strong {
|
|
||||||
font-size: 30px;
|
|
||||||
line-height: 1;
|
|
||||||
letter-spacing: -0.05em;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.is-positive strong {
|
|
||||||
color: var(--success);
|
|
||||||
}
|
|
||||||
|
|
||||||
&.is-danger strong {
|
|
||||||
color: var(--danger);
|
|
||||||
}
|
|
||||||
|
|
||||||
&.is-brand strong {
|
|
||||||
color: var(--brand-deep);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.summary-label {
|
|
||||||
font-size: 12px;
|
|
||||||
font-weight: 800;
|
|
||||||
text-transform: uppercase;
|
|
||||||
letter-spacing: 0.08em;
|
|
||||||
color: var(--muted);
|
|
||||||
}
|
|
||||||
|
|
||||||
.toolbar {
|
|
||||||
padding: 14px 16px;
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
gap: 12px;
|
|
||||||
align-items: flex-start;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.toolbar-left {
|
|
||||||
display: grid;
|
|
||||||
gap: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.view-tabs {
|
|
||||||
display: inline-flex;
|
|
||||||
gap: 8px;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.type-filters {
|
|
||||||
display: flex;
|
|
||||||
gap: 10px;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.type-filter {
|
|
||||||
border: 1px solid rgba(24, 17, 33, 0.08);
|
|
||||||
background: linear-gradient(180deg, rgba(255, 255, 255, 0.96), rgba(246, 244, 248, 0.92));
|
|
||||||
color: var(--ink);
|
|
||||||
border-radius: 18px;
|
|
||||||
padding: 10px 14px;
|
|
||||||
min-width: 152px;
|
|
||||||
display: grid;
|
|
||||||
gap: 2px;
|
|
||||||
justify-items: start;
|
|
||||||
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.72);
|
|
||||||
|
|
||||||
span {
|
|
||||||
font-size: 11px;
|
|
||||||
font-weight: 900;
|
|
||||||
text-transform: uppercase;
|
|
||||||
letter-spacing: 0.08em;
|
|
||||||
color: var(--muted);
|
|
||||||
}
|
|
||||||
|
|
||||||
strong {
|
|
||||||
font-size: 16px;
|
|
||||||
letter-spacing: -0.03em;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.active {
|
|
||||||
transform: translateY(-1px);
|
|
||||||
}
|
|
||||||
|
|
||||||
&.is-all.active {
|
|
||||||
background: rgba(151, 38, 136, 0.12);
|
|
||||||
border-color: rgba(151, 38, 136, 0.24);
|
|
||||||
color: var(--brand-deep);
|
|
||||||
}
|
|
||||||
|
|
||||||
&.is-line.active {
|
|
||||||
background: rgba(3, 15, 170, 0.1);
|
|
||||||
border-color: rgba(3, 15, 170, 0.2);
|
|
||||||
color: #030faa;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.is-chip.active {
|
|
||||||
background: rgba(255, 178, 0, 0.14);
|
|
||||||
border-color: rgba(255, 178, 0, 0.26);
|
|
||||||
color: #8c6200;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.is-status.active {
|
|
||||||
background: rgba(220, 53, 69, 0.12);
|
|
||||||
border-color: rgba(220, 53, 69, 0.22);
|
|
||||||
color: #9f1d2d;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.filter-tab {
|
|
||||||
border: 1px solid rgba(24, 17, 33, 0.08);
|
|
||||||
background: rgba(24, 17, 33, 0.04);
|
|
||||||
color: var(--ink);
|
|
||||||
border-radius: 999px;
|
|
||||||
padding: 8px 14px;
|
|
||||||
font-size: 12px;
|
|
||||||
font-weight: 800;
|
|
||||||
|
|
||||||
&.active {
|
|
||||||
background: rgba(227, 61, 207, 0.14);
|
|
||||||
border-color: rgba(227, 61, 207, 0.24);
|
|
||||||
color: var(--brand-deep);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.toolbar-right {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 10px;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.search-group {
|
|
||||||
min-width: min(360px, 86vw);
|
|
||||||
}
|
|
||||||
|
|
||||||
.page-size-select {
|
|
||||||
min-width: 140px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.table-wrap {
|
|
||||||
overflow: hidden;
|
|
||||||
max-width: 1120px;
|
|
||||||
margin-left: auto;
|
|
||||||
margin-right: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.table-modern {
|
|
||||||
margin: 0;
|
|
||||||
width: 100%;
|
|
||||||
table-layout: fixed;
|
|
||||||
|
|
||||||
thead th:nth-child(1) {
|
|
||||||
width: 16%;
|
|
||||||
}
|
|
||||||
|
|
||||||
thead th:nth-child(2),
|
|
||||||
thead th:nth-child(3) {
|
|
||||||
width: 24%;
|
|
||||||
}
|
|
||||||
|
|
||||||
thead th:nth-child(4) {
|
|
||||||
width: 24%;
|
|
||||||
}
|
|
||||||
|
|
||||||
thead th:nth-child(5) {
|
|
||||||
width: 12%;
|
|
||||||
}
|
|
||||||
|
|
||||||
thead th {
|
|
||||||
background: #faf7fc;
|
|
||||||
border-bottom: 1px solid var(--line);
|
|
||||||
color: rgba(24, 17, 33, 0.72);
|
|
||||||
font-size: 12px;
|
|
||||||
font-weight: 900;
|
|
||||||
text-transform: uppercase;
|
|
||||||
letter-spacing: 0.06em;
|
|
||||||
padding: 14px 16px;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
tbody tr {
|
|
||||||
transition: background-color 160ms ease;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
background: rgba(151, 38, 136, 0.03);
|
|
||||||
}
|
|
||||||
|
|
||||||
&.row-applied {
|
|
||||||
background: rgba(25, 135, 84, 0.03);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
tbody td {
|
|
||||||
padding: 16px;
|
|
||||||
border-top: 1px solid rgba(24, 17, 33, 0.06);
|
|
||||||
vertical-align: middle;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.cell-line,
|
|
||||||
.cell-situation,
|
|
||||||
.cell-action {
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.cell-compare {
|
|
||||||
text-align: left;
|
|
||||||
}
|
|
||||||
|
|
||||||
.line-cell-stack {
|
|
||||||
display: grid;
|
|
||||||
gap: 10px;
|
|
||||||
justify-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.line-number-chip {
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
min-width: 120px;
|
|
||||||
padding: 8px 14px;
|
|
||||||
border-radius: 999px;
|
|
||||||
background: rgba(3, 15, 170, 0.08);
|
|
||||||
border: 1px solid rgba(3, 15, 170, 0.18);
|
|
||||||
color: #030faa;
|
|
||||||
font-size: 0.83rem;
|
|
||||||
font-weight: 950;
|
|
||||||
letter-spacing: 0.02em;
|
|
||||||
box-shadow:
|
|
||||||
0 8px 18px rgba(3, 15, 170, 0.08),
|
|
||||||
inset 0 1px 0 rgba(255, 255, 255, 0.7);
|
|
||||||
}
|
|
||||||
|
|
||||||
.issue-kind-badge {
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
padding: 6px 10px;
|
|
||||||
border-radius: 999px;
|
|
||||||
font-size: 11px;
|
|
||||||
font-weight: 900;
|
|
||||||
line-height: 1;
|
|
||||||
border: 1px solid transparent;
|
|
||||||
}
|
|
||||||
|
|
||||||
.issue-kind-badge {
|
|
||||||
&.is-line {
|
|
||||||
background: rgba(3, 15, 170, 0.1);
|
|
||||||
border-color: rgba(3, 15, 170, 0.2);
|
|
||||||
color: #030faa;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.is-chip {
|
|
||||||
background: rgba(255, 178, 0, 0.16);
|
|
||||||
border-color: rgba(255, 178, 0, 0.26);
|
|
||||||
color: #8c6200;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.is-status {
|
|
||||||
background: rgba(220, 53, 69, 0.12);
|
|
||||||
border-color: rgba(220, 53, 69, 0.2);
|
|
||||||
color: #9f1d2d;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.is-review,
|
|
||||||
&.is-neutral {
|
|
||||||
background: rgba(24, 17, 33, 0.06);
|
|
||||||
border-color: rgba(24, 17, 33, 0.08);
|
|
||||||
color: rgba(24, 17, 33, 0.7);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.status-pill {
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
padding: 8px 14px;
|
|
||||||
border-radius: 999px;
|
|
||||||
font-size: 12px;
|
|
||||||
font-weight: 900;
|
|
||||||
border: 1px solid transparent;
|
|
||||||
|
|
||||||
&.is-active {
|
|
||||||
background: rgba(25, 135, 84, 0.12);
|
|
||||||
color: #11653d;
|
|
||||||
border-color: rgba(25, 135, 84, 0.18);
|
|
||||||
}
|
|
||||||
|
|
||||||
&.is-blocked {
|
|
||||||
background: rgba(220, 53, 69, 0.12);
|
|
||||||
color: #9f1d2d;
|
|
||||||
border-color: rgba(220, 53, 69, 0.18);
|
|
||||||
}
|
|
||||||
|
|
||||||
&.is-neutral {
|
|
||||||
background: rgba(24, 17, 33, 0.06);
|
|
||||||
color: rgba(24, 17, 33, 0.72);
|
|
||||||
border-color: rgba(24, 17, 33, 0.08);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.issue-card {
|
|
||||||
display: grid;
|
|
||||||
gap: 10px;
|
|
||||||
padding: 14px;
|
|
||||||
border-radius: 18px;
|
|
||||||
border: 1px solid rgba(24, 17, 33, 0.08);
|
|
||||||
background: linear-gradient(180deg, rgba(255, 255, 255, 0.96), rgba(248, 246, 250, 0.92));
|
|
||||||
box-shadow: 0 14px 28px rgba(24, 17, 33, 0.06);
|
|
||||||
text-align: left;
|
|
||||||
}
|
|
||||||
|
|
||||||
.issue-card-system {
|
|
||||||
border-color: rgba(3, 15, 170, 0.12);
|
|
||||||
}
|
|
||||||
|
|
||||||
.issue-card-report {
|
|
||||||
border-color: rgba(151, 38, 136, 0.14);
|
|
||||||
}
|
|
||||||
|
|
||||||
.issue-card-head {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
gap: 10px;
|
|
||||||
padding-bottom: 10px;
|
|
||||||
border-bottom: 1px solid rgba(24, 17, 33, 0.08);
|
|
||||||
}
|
|
||||||
|
|
||||||
.issue-card-eyebrow {
|
|
||||||
font-size: 11px;
|
|
||||||
font-weight: 900;
|
|
||||||
text-transform: uppercase;
|
|
||||||
letter-spacing: 0.08em;
|
|
||||||
color: var(--brand-deep);
|
|
||||||
}
|
|
||||||
|
|
||||||
.issue-card-caption {
|
|
||||||
font-size: 12px;
|
|
||||||
font-weight: 700;
|
|
||||||
color: var(--muted);
|
|
||||||
}
|
|
||||||
|
|
||||||
.issue-row {
|
|
||||||
display: grid;
|
|
||||||
gap: 8px;
|
|
||||||
padding: 11px 12px;
|
|
||||||
border-radius: 14px;
|
|
||||||
background: rgba(255, 255, 255, 0.86);
|
|
||||||
border: 1px solid rgba(24, 17, 33, 0.06);
|
|
||||||
|
|
||||||
&.is-different {
|
|
||||||
background: linear-gradient(180deg, rgba(227, 61, 207, 0.08), rgba(255, 255, 255, 0.96));
|
|
||||||
border-color: rgba(227, 61, 207, 0.22);
|
|
||||||
box-shadow: inset 0 0 0 1px rgba(227, 61, 207, 0.08);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.issue-row-head {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
gap: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.issue-label {
|
|
||||||
font-size: 11px;
|
|
||||||
font-weight: 800;
|
|
||||||
text-transform: uppercase;
|
|
||||||
letter-spacing: 0.08em;
|
|
||||||
color: var(--muted);
|
|
||||||
}
|
|
||||||
|
|
||||||
.field-diff-flag {
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
padding: 4px 8px;
|
|
||||||
border-radius: 999px;
|
|
||||||
background: rgba(227, 61, 207, 0.12);
|
|
||||||
color: var(--brand-deep);
|
|
||||||
font-size: 10px;
|
|
||||||
font-weight: 900;
|
|
||||||
text-transform: uppercase;
|
|
||||||
letter-spacing: 0.08em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.issue-value,
|
|
||||||
.issue-notes {
|
|
||||||
font-size: 13px;
|
|
||||||
line-height: 1.45;
|
|
||||||
color: rgba(24, 17, 33, 0.8);
|
|
||||||
}
|
|
||||||
|
|
||||||
.issue-value {
|
|
||||||
font-weight: 800;
|
|
||||||
word-break: break-word;
|
|
||||||
}
|
|
||||||
|
|
||||||
.issue-notes {
|
|
||||||
color: var(--muted);
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.situation-card {
|
|
||||||
display: grid;
|
|
||||||
gap: 12px;
|
|
||||||
padding: 14px;
|
|
||||||
border-radius: 18px;
|
|
||||||
background: linear-gradient(180deg, rgba(255, 255, 255, 0.97), rgba(248, 246, 250, 0.93));
|
|
||||||
box-shadow: 0 14px 28px rgba(24, 17, 33, 0.05);
|
|
||||||
justify-items: center;
|
|
||||||
text-align: center;
|
|
||||||
|
|
||||||
&.is-applied {
|
|
||||||
background: linear-gradient(180deg, rgba(25, 135, 84, 0.06), rgba(255, 255, 255, 0.98));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.situation-top {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
gap: 10px;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.action-card {
|
|
||||||
display: grid;
|
|
||||||
gap: 10px;
|
|
||||||
justify-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sync-badge {
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
padding: 7px 12px;
|
|
||||||
border-radius: 999px;
|
|
||||||
font-size: 12px;
|
|
||||||
font-weight: 900;
|
|
||||||
|
|
||||||
&.ready {
|
|
||||||
background: rgba(255, 178, 0, 0.16);
|
|
||||||
color: #8c6200;
|
|
||||||
font-size: 11px;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.applied {
|
|
||||||
background: rgba(25, 135, 84, 0.14);
|
|
||||||
color: #11653d;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.muted {
|
|
||||||
background: rgba(24, 17, 33, 0.06);
|
|
||||||
color: rgba(24, 17, 33, 0.62);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.page-footer {
|
|
||||||
margin-top: 16px;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
gap: 12px;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.status-empty,
|
|
||||||
.empty-state {
|
|
||||||
padding: 42px 20px;
|
|
||||||
text-align: center;
|
|
||||||
color: var(--muted);
|
|
||||||
display: grid;
|
|
||||||
gap: 8px;
|
|
||||||
place-items: center;
|
|
||||||
|
|
||||||
i {
|
|
||||||
font-size: 34px;
|
|
||||||
color: var(--brand-deep);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.empty-state small {
|
|
||||||
max-width: 560px;
|
|
||||||
line-height: 1.5;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 1100px) {
|
|
||||||
.header-top {
|
|
||||||
grid-template-columns: 1fr;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.title-badge,
|
|
||||||
.header-actions {
|
|
||||||
justify-self: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.summary-grid {
|
|
||||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
||||||
}
|
|
||||||
|
|
||||||
.upload-grid {
|
|
||||||
grid-template-columns: 1fr;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 720px) {
|
|
||||||
.page-shell {
|
|
||||||
width: calc(100vw - 16px);
|
|
||||||
margin-top: 18px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.page-header,
|
|
||||||
.page-body {
|
|
||||||
padding-left: 14px;
|
|
||||||
padding-right: 14px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.summary-grid {
|
|
||||||
grid-template-columns: 1fr;
|
|
||||||
}
|
|
||||||
|
|
||||||
.page-card {
|
|
||||||
border-radius: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.page-header > *,
|
|
||||||
.page-body > * {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.toolbar-right,
|
|
||||||
.header-actions {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.header-actions .btn,
|
|
||||||
.upload-actions .btn {
|
|
||||||
width: 100%;
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.upload-zone {
|
|
||||||
min-height: 150px;
|
|
||||||
padding: 18px 14px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.upload-title {
|
|
||||||
font-size: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.intro-meta,
|
|
||||||
.secondary-notes,
|
|
||||||
.view-tabs,
|
|
||||||
.toolbar,
|
|
||||||
.type-filters {
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.view-tabs {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.toolbar-left {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.type-filters {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.filter-tab {
|
|
||||||
flex: 1 1 120px;
|
|
||||||
justify-content: center;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.type-filter {
|
|
||||||
flex: 1 1 152px;
|
|
||||||
min-width: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.search-group,
|
|
||||||
.page-size-select {
|
|
||||||
width: 100%;
|
|
||||||
min-width: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.table-wrap {
|
|
||||||
overflow-x: auto;
|
|
||||||
-webkit-overflow-scrolling: touch;
|
|
||||||
border-radius: 18px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.table-modern {
|
|
||||||
min-width: 1180px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.line-number-chip {
|
|
||||||
min-width: 108px;
|
|
||||||
font-size: 0.78rem;
|
|
||||||
padding: 7px 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.page-footer {
|
|
||||||
justify-content: center;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 420px) {
|
|
||||||
.mve-page {
|
|
||||||
padding-left: 8px;
|
|
||||||
padding-right: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.page-header,
|
|
||||||
.page-body {
|
|
||||||
padding-left: 12px;
|
|
||||||
padding-right: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.title {
|
|
||||||
font-size: 24px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.subtitle {
|
|
||||||
font-size: 0.88rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.title-badge,
|
|
||||||
.meta-pill,
|
|
||||||
.filter-tab,
|
|
||||||
.status-pill,
|
|
||||||
.sync-badge {
|
|
||||||
font-size: 11px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.page-footer .pagination {
|
|
||||||
justify-content: center;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,477 +0,0 @@
|
||||||
import { Component, ChangeDetectorRef, ElementRef, Inject, OnInit, PLATFORM_ID, ViewChild } 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 {
|
|
||||||
MveAuditService,
|
|
||||||
type ApplyMveAuditResult,
|
|
||||||
type MveAuditIssue,
|
|
||||||
type MveAuditRun,
|
|
||||||
} from '../../services/mve-audit.service';
|
|
||||||
import { confirmActionModal } from '../../utils/destructive-confirmation';
|
|
||||||
import {
|
|
||||||
buildPageNumbers,
|
|
||||||
clampPage,
|
|
||||||
computePageEnd,
|
|
||||||
computePageStart,
|
|
||||||
computeTotalPages,
|
|
||||||
} from '../../utils/pagination.util';
|
|
||||||
|
|
||||||
type MveIssueViewMode = 'PENDING' | 'APPLIED' | 'ALL';
|
|
||||||
type MveIssueCategory = 'ALL' | 'STATUS' | 'LINE' | 'CHIP';
|
|
||||||
|
|
||||||
@Component({
|
|
||||||
selector: 'app-mve-auditoria',
|
|
||||||
standalone: true,
|
|
||||||
imports: [CommonModule, FormsModule],
|
|
||||||
templateUrl: './mve-auditoria.html',
|
|
||||||
styleUrls: ['./mve-auditoria.scss'],
|
|
||||||
})
|
|
||||||
export class MveAuditoriaPage implements OnInit {
|
|
||||||
@ViewChild('feedbackToast', { static: false }) feedbackToast?: ElementRef<HTMLElement>;
|
|
||||||
|
|
||||||
loadingLatest = false;
|
|
||||||
processing = false;
|
|
||||||
syncing = false;
|
|
||||||
selectedFile: File | null = null;
|
|
||||||
auditResult: MveAuditRun | null = null;
|
|
||||||
applyResult: ApplyMveAuditResult | null = null;
|
|
||||||
errorMessage = '';
|
|
||||||
toastMessage = '';
|
|
||||||
|
|
||||||
searchTerm = '';
|
|
||||||
viewMode: MveIssueViewMode = 'PENDING';
|
|
||||||
issueCategory: MveIssueCategory = 'ALL';
|
|
||||||
page = 1;
|
|
||||||
pageSize = 20;
|
|
||||||
readonly pageSizeOptions = [10, 20, 50, 100];
|
|
||||||
|
|
||||||
constructor(
|
|
||||||
private readonly mveAuditService: MveAuditService,
|
|
||||||
private readonly cdr: ChangeDetectorRef,
|
|
||||||
@Inject(PLATFORM_ID) private readonly platformId: object
|
|
||||||
) {}
|
|
||||||
|
|
||||||
ngOnInit(): void {
|
|
||||||
this.errorMessage = '';
|
|
||||||
const cachedRun = this.mveAuditService.getCachedRun();
|
|
||||||
if (cachedRun) {
|
|
||||||
this.auditResult = cachedRun;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
void this.restoreCachedAudit();
|
|
||||||
}
|
|
||||||
|
|
||||||
get hasAuditResult(): boolean {
|
|
||||||
return !!this.auditResult;
|
|
||||||
}
|
|
||||||
|
|
||||||
get syncableIssues(): MveAuditIssue[] {
|
|
||||||
return this.relevantIssues.filter((issue) => issue.syncable && !issue.applied);
|
|
||||||
}
|
|
||||||
|
|
||||||
get relevantIssues(): MveAuditIssue[] {
|
|
||||||
const issues = this.auditResult?.issues ?? [];
|
|
||||||
return issues
|
|
||||||
.filter((issue) => this.issueHasRelevantDifference(issue))
|
|
||||||
.sort((left, right) => Number(left.applied) - Number(right.applied));
|
|
||||||
}
|
|
||||||
|
|
||||||
get filteredIssues(): MveAuditIssue[] {
|
|
||||||
const query = this.normalizeSearch(this.searchTerm);
|
|
||||||
return this.relevantIssues.filter((issue) => {
|
|
||||||
if (this.viewMode === 'PENDING' && issue.applied) return false;
|
|
||||||
if (this.viewMode === 'APPLIED' && !issue.applied) return false;
|
|
||||||
if (!this.matchesIssueCategory(issue)) return false;
|
|
||||||
if (!query) return true;
|
|
||||||
|
|
||||||
const haystack = [
|
|
||||||
issue.numeroLinha,
|
|
||||||
issue.issueType,
|
|
||||||
issue.actionSuggestion,
|
|
||||||
issue.systemStatus,
|
|
||||||
issue.reportStatus,
|
|
||||||
issue.systemSnapshot?.numeroLinha,
|
|
||||||
issue.reportSnapshot?.numeroLinha,
|
|
||||||
issue.systemSnapshot?.chip,
|
|
||||||
issue.reportSnapshot?.chip,
|
|
||||||
issue.situation,
|
|
||||||
issue.notes,
|
|
||||||
...(issue.differences ?? []).flatMap((difference) => [
|
|
||||||
difference.label,
|
|
||||||
difference.systemValue,
|
|
||||||
difference.reportValue,
|
|
||||||
]),
|
|
||||||
]
|
|
||||||
.map((value) => this.normalizeSearch(value))
|
|
||||||
.join(' ');
|
|
||||||
|
|
||||||
return haystack.includes(query);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
get pagedIssues(): MveAuditIssue[] {
|
|
||||||
const offset = (this.page - 1) * this.pageSize;
|
|
||||||
return this.filteredIssues.slice(offset, offset + this.pageSize);
|
|
||||||
}
|
|
||||||
|
|
||||||
get totalPages(): number {
|
|
||||||
return computeTotalPages(this.filteredIssues.length, this.pageSize);
|
|
||||||
}
|
|
||||||
|
|
||||||
get pageNumbers(): number[] {
|
|
||||||
return buildPageNumbers(this.page, this.totalPages);
|
|
||||||
}
|
|
||||||
|
|
||||||
get pageStart(): number {
|
|
||||||
return computePageStart(this.filteredIssues.length, this.page, this.pageSize);
|
|
||||||
}
|
|
||||||
|
|
||||||
get pageEnd(): number {
|
|
||||||
return computePageEnd(this.filteredIssues.length, this.page, this.pageSize);
|
|
||||||
}
|
|
||||||
|
|
||||||
get totalDifferencesCount(): number {
|
|
||||||
if (!this.auditResult) return 0;
|
|
||||||
return this.auditResult.summary.totalStatusDivergences + this.auditResult.summary.totalDataDivergences;
|
|
||||||
}
|
|
||||||
|
|
||||||
get manualReviewIssuesCount(): number {
|
|
||||||
return this.relevantIssues.filter((issue) => !issue.syncable && !issue.applied).length;
|
|
||||||
}
|
|
||||||
|
|
||||||
get statusIssuesCount(): number {
|
|
||||||
return this.relevantIssues.filter((issue) => this.hasDifference(issue, 'status')).length;
|
|
||||||
}
|
|
||||||
|
|
||||||
get lineIssuesCount(): number {
|
|
||||||
return this.relevantIssues.filter((issue) => this.hasDifference(issue, 'line')).length;
|
|
||||||
}
|
|
||||||
|
|
||||||
get chipIssuesCount(): number {
|
|
||||||
return this.relevantIssues.filter((issue) => this.hasDifference(issue, 'chip')).length;
|
|
||||||
}
|
|
||||||
|
|
||||||
get ignoredIssuesCount(): number {
|
|
||||||
if (!this.auditResult) return 0;
|
|
||||||
const summary = this.auditResult.summary;
|
|
||||||
return (
|
|
||||||
summary.totalOnlyInSystem +
|
|
||||||
summary.totalOnlyInReport +
|
|
||||||
summary.totalDuplicateReportLines +
|
|
||||||
summary.totalDuplicateSystemLines +
|
|
||||||
summary.totalInvalidRows +
|
|
||||||
summary.totalUnknownStatuses
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
async loadLatestAudit(): Promise<void> {
|
|
||||||
this.loadingLatest = true;
|
|
||||||
this.errorMessage = '';
|
|
||||||
|
|
||||||
try {
|
|
||||||
this.auditResult = await firstValueFrom(this.mveAuditService.getLatest());
|
|
||||||
this.applyResult = null;
|
|
||||||
this.issueCategory = 'ALL';
|
|
||||||
this.page = 1;
|
|
||||||
} catch (error) {
|
|
||||||
const httpError = error as HttpErrorResponse | null;
|
|
||||||
if (httpError?.status !== 404) {
|
|
||||||
this.errorMessage = this.extractHttpMessage(error, 'Nao foi possivel carregar a ultima conferencia.');
|
|
||||||
}
|
|
||||||
this.auditResult = null;
|
|
||||||
} finally {
|
|
||||||
this.loadingLatest = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onFileSelected(event: Event): void {
|
|
||||||
const input = event.target as HTMLInputElement | null;
|
|
||||||
const file = input?.files?.[0] ?? null;
|
|
||||||
|
|
||||||
this.errorMessage = '';
|
|
||||||
this.applyResult = null;
|
|
||||||
|
|
||||||
if (!file) {
|
|
||||||
this.selectedFile = null;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!file.name.toLowerCase().endsWith('.csv')) {
|
|
||||||
this.selectedFile = null;
|
|
||||||
this.errorMessage = 'Selecione o relatorio exportado pela Vivo.';
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (file.size <= 0) {
|
|
||||||
this.selectedFile = null;
|
|
||||||
this.errorMessage = 'O arquivo selecionado está vazio.';
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (file.size > 20 * 1024 * 1024) {
|
|
||||||
this.selectedFile = null;
|
|
||||||
this.errorMessage = 'O arquivo excede o limite de 20 MB.';
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.selectedFile = file;
|
|
||||||
}
|
|
||||||
|
|
||||||
clearSelectedFile(): void {
|
|
||||||
this.selectedFile = null;
|
|
||||||
this.errorMessage = '';
|
|
||||||
}
|
|
||||||
|
|
||||||
async processAudit(): Promise<void> {
|
|
||||||
if (!this.selectedFile || this.processing || this.syncing) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.processing = true;
|
|
||||||
this.errorMessage = '';
|
|
||||||
this.applyResult = null;
|
|
||||||
|
|
||||||
try {
|
|
||||||
this.auditResult = await firstValueFrom(this.mveAuditService.preview(this.selectedFile));
|
|
||||||
this.issueCategory = 'ALL';
|
|
||||||
this.page = 1;
|
|
||||||
this.viewMode = 'PENDING';
|
|
||||||
this.searchTerm = '';
|
|
||||||
await this.showToast('Relatorio conferido com sucesso.');
|
|
||||||
} catch (error) {
|
|
||||||
this.errorMessage = this.extractHttpMessage(error, 'Nao foi possivel conferir o relatorio.');
|
|
||||||
} finally {
|
|
||||||
this.processing = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async syncIssues(): Promise<void> {
|
|
||||||
if (!this.auditResult || this.syncableIssues.length === 0 || this.syncing) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const confirmed = await confirmActionModal({
|
|
||||||
title: 'Atualizar sistema',
|
|
||||||
message: `${this.syncableIssues.length} ocorrência(s) sincronizável(is) serão aplicadas com base no relatório da Vivo.`,
|
|
||||||
confirmLabel: 'Atualizar agora',
|
|
||||||
cancelLabel: 'Cancelar',
|
|
||||||
tone: 'warning',
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!confirmed) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.syncing = true;
|
|
||||||
this.errorMessage = '';
|
|
||||||
|
|
||||||
try {
|
|
||||||
this.applyResult = await firstValueFrom(this.mveAuditService.apply(this.auditResult.id));
|
|
||||||
this.auditResult = await firstValueFrom(this.mveAuditService.getById(this.auditResult.id));
|
|
||||||
this.viewMode = 'ALL';
|
|
||||||
this.page = 1;
|
|
||||||
await this.showToast('Atualizações aplicadas com sucesso.');
|
|
||||||
} catch (error) {
|
|
||||||
this.errorMessage = this.extractHttpMessage(error, 'Nao foi possivel atualizar o sistema.');
|
|
||||||
} finally {
|
|
||||||
this.syncing = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onSearchChange(): void {
|
|
||||||
this.page = 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
onPageSizeChange(): void {
|
|
||||||
this.page = 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
setViewMode(mode: MveIssueViewMode): void {
|
|
||||||
this.viewMode = mode;
|
|
||||||
this.page = 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
setIssueCategory(category: MveIssueCategory): void {
|
|
||||||
this.issueCategory = category;
|
|
||||||
this.page = 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
goToPage(page: number): void {
|
|
||||||
this.page = clampPage(page, this.totalPages);
|
|
||||||
}
|
|
||||||
|
|
||||||
trackByIssue(_: number, issue: MveAuditIssue): string {
|
|
||||||
return issue.id;
|
|
||||||
}
|
|
||||||
|
|
||||||
formatDateTime(value?: string | null): string {
|
|
||||||
if (!value) return '-';
|
|
||||||
const date = new Date(value);
|
|
||||||
if (Number.isNaN(date.getTime())) return '-';
|
|
||||||
return new Intl.DateTimeFormat('pt-BR', {
|
|
||||||
dateStyle: 'short',
|
|
||||||
timeStyle: 'short',
|
|
||||||
}).format(date);
|
|
||||||
}
|
|
||||||
|
|
||||||
statusClass(status: string | null | undefined): string {
|
|
||||||
const normalized = (status ?? '').toLowerCase();
|
|
||||||
if (normalized.includes('bloq') || normalized.includes('perda') || normalized.includes('roubo')) return 'is-blocked';
|
|
||||||
if (normalized.includes('ativo')) return 'is-active';
|
|
||||||
return 'is-neutral';
|
|
||||||
}
|
|
||||||
|
|
||||||
statusLabel(status: string | null | undefined): string {
|
|
||||||
const value = (status ?? '').trim();
|
|
||||||
return value || '-';
|
|
||||||
}
|
|
||||||
|
|
||||||
hasDifference(issue: MveAuditIssue, fieldKey: string): boolean {
|
|
||||||
return (issue.differences ?? []).some((difference) => difference.fieldKey === fieldKey);
|
|
||||||
}
|
|
||||||
|
|
||||||
formatValue(value?: string | null): string {
|
|
||||||
const normalized = (value ?? '').trim();
|
|
||||||
return normalized || '-';
|
|
||||||
}
|
|
||||||
|
|
||||||
issueKindLabel(issue: MveAuditIssue): string {
|
|
||||||
const hasLine = this.hasDifference(issue, 'line');
|
|
||||||
const hasChip = this.hasDifference(issue, 'chip');
|
|
||||||
const hasStatus = this.hasDifference(issue, 'status');
|
|
||||||
|
|
||||||
if (hasLine && hasStatus) return 'Troca de linha + status';
|
|
||||||
if (hasChip && hasStatus) return 'Troca de chip + status';
|
|
||||||
if (hasLine) return 'Troca de linha';
|
|
||||||
if (hasChip) return 'Troca de chip';
|
|
||||||
if (hasStatus) return 'Status';
|
|
||||||
if (issue.issueType === 'DDD_CHANGE_REVIEW') return 'Revisão de DDD';
|
|
||||||
return 'Revisão';
|
|
||||||
}
|
|
||||||
|
|
||||||
issueKindClass(issue: MveAuditIssue): string {
|
|
||||||
if (this.hasDifference(issue, 'line')) return 'is-line';
|
|
||||||
if (this.hasDifference(issue, 'chip')) return 'is-chip';
|
|
||||||
if (this.hasDifference(issue, 'status')) return 'is-status';
|
|
||||||
return issue.syncable ? 'is-neutral' : 'is-review';
|
|
||||||
}
|
|
||||||
|
|
||||||
situationClass(issue: MveAuditIssue): string {
|
|
||||||
if (issue.applied) return 'is-applied';
|
|
||||||
if (!issue.syncable) return 'is-review';
|
|
||||||
return this.issueKindClass(issue);
|
|
||||||
}
|
|
||||||
|
|
||||||
severityClass(severity: string | null | undefined): string {
|
|
||||||
const normalized = (severity ?? '').trim().toUpperCase();
|
|
||||||
if (normalized === 'HIGH') return 'is-high';
|
|
||||||
if (normalized === 'MEDIUM') return 'is-medium';
|
|
||||||
if (normalized === 'WARNING') return 'is-warning';
|
|
||||||
return 'is-neutral';
|
|
||||||
}
|
|
||||||
|
|
||||||
severityLabel(severity: string | null | undefined): string {
|
|
||||||
const normalized = (severity ?? '').trim().toUpperCase();
|
|
||||||
if (normalized === 'HIGH') return 'Alta';
|
|
||||||
if (normalized === 'MEDIUM') return 'Media';
|
|
||||||
if (normalized === 'WARNING') return 'Aviso';
|
|
||||||
return 'Info';
|
|
||||||
}
|
|
||||||
|
|
||||||
describeIssue(issue: MveAuditIssue): string {
|
|
||||||
const differences = issue.differences ?? [];
|
|
||||||
if (!differences.length) {
|
|
||||||
return issue.notes?.trim() || 'Sem diferenças detalhadas.';
|
|
||||||
}
|
|
||||||
|
|
||||||
return differences
|
|
||||||
.map((difference) => `${difference.label}: ${this.formatValue(difference.systemValue)} -> ${this.formatValue(difference.reportValue)}`)
|
|
||||||
.join(' | ');
|
|
||||||
}
|
|
||||||
|
|
||||||
private issueHasRelevantDifference(issue: MveAuditIssue): boolean {
|
|
||||||
return (issue.differences ?? []).length > 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
private async restoreCachedAudit(): Promise<void> {
|
|
||||||
const cachedRunId = this.mveAuditService.getCachedRunId();
|
|
||||||
if (!cachedRunId) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.loadingLatest = true;
|
|
||||||
|
|
||||||
try {
|
|
||||||
const restoredRun = await firstValueFrom(this.mveAuditService.restoreCachedRun());
|
|
||||||
if (!restoredRun) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.auditResult = restoredRun;
|
|
||||||
this.applyResult = null;
|
|
||||||
this.issueCategory = 'ALL';
|
|
||||||
this.page = 1;
|
|
||||||
} finally {
|
|
||||||
this.loadingLatest = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private matchesIssueCategory(issue: MveAuditIssue): boolean {
|
|
||||||
switch (this.issueCategory) {
|
|
||||||
case 'STATUS':
|
|
||||||
return this.hasDifference(issue, 'status');
|
|
||||||
case 'LINE':
|
|
||||||
return this.hasDifference(issue, 'line');
|
|
||||||
case 'CHIP':
|
|
||||||
return this.hasDifference(issue, 'chip');
|
|
||||||
default:
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private normalizeSearch(value: unknown): string {
|
|
||||||
return (value ?? '')
|
|
||||||
.toString()
|
|
||||||
.normalize('NFD')
|
|
||||||
.replace(/[\u0300-\u036f]/g, '')
|
|
||||||
.toLowerCase()
|
|
||||||
.trim();
|
|
||||||
}
|
|
||||||
|
|
||||||
private extractHttpMessage(error: unknown, fallbackMessage: string): string {
|
|
||||||
const httpError = error as HttpErrorResponse | null;
|
|
||||||
if (httpError?.status === 0) {
|
|
||||||
return 'A API do LineGestao nao respondeu em http://localhost:5298. Inicie o backend e tente novamente.';
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
(httpError?.error as { message?: string } | null)?.message ||
|
|
||||||
httpError?.message ||
|
|
||||||
fallbackMessage
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
private async showToast(message: string): Promise<void> {
|
|
||||||
if (!isPlatformBrowser(this.platformId)) return;
|
|
||||||
|
|
||||||
this.toastMessage = message;
|
|
||||||
this.cdr.detectChanges();
|
|
||||||
|
|
||||||
if (!this.feedbackToast?.nativeElement) return;
|
|
||||||
|
|
||||||
try {
|
|
||||||
const bootstrap = await import('bootstrap');
|
|
||||||
const instance = bootstrap.Toast.getOrCreateInstance(this.feedbackToast.nativeElement, {
|
|
||||||
autohide: true,
|
|
||||||
delay: 3200,
|
|
||||||
});
|
|
||||||
instance.show();
|
|
||||||
} catch {
|
|
||||||
// ignora falha de feedback visual
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -69,7 +69,7 @@
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="bulk-btn ghost export-glass"
|
class="bulk-btn ghost"
|
||||||
(click)="exportNotifications()"
|
(click)="exportNotifications()"
|
||||||
[disabled]="exportLoading || filteredNotifications.length === 0"
|
[disabled]="exportLoading || filteredNotifications.length === 0"
|
||||||
>
|
>
|
||||||
|
|
|
||||||
|
|
@ -96,13 +96,6 @@ $border: #e5e7eb;
|
||||||
&:disabled { opacity: 0.6; cursor: default; }
|
&:disabled { opacity: 0.6; cursor: default; }
|
||||||
|
|
||||||
&.ghost { background: transparent; }
|
&.ghost { background: transparent; }
|
||||||
&.export-glass {
|
|
||||||
background: rgba(255, 255, 255, 0.7);
|
|
||||||
border-color: rgba(28, 56, 201, 0.22);
|
|
||||||
color: $primary;
|
|
||||||
font-weight: 800;
|
|
||||||
box-shadow: 0 6px 14px rgba(15, 23, 42, 0.08);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* FILTROS (Estilo Tabs/Pills) */
|
/* FILTROS (Estilo Tabs/Pills) */
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,6 @@
|
||||||
<app-modal-layer
|
<div class="lg-backdrop" *ngIf="open" (click)="close.emit()"></div>
|
||||||
[open]="open"
|
<div class="lg-modal" *ngIf="open">
|
||||||
backdropClass="lg-backdrop"
|
<div class="lg-modal-card" (click)="$event.stopPropagation()">
|
||||||
overlayClass="lg-modal"
|
|
||||||
(close)="close.emit()"
|
|
||||||
>
|
|
||||||
<div *ngIf="open" class="lg-modal-card" (click)="$event.stopPropagation()">
|
|
||||||
<div class="modal-header">
|
<div class="modal-header">
|
||||||
<div class="modal-title">
|
<div class="modal-title">
|
||||||
<span class="icon-bg"><i class="bi bi-plus-circle"></i></span>
|
<span class="icon-bg"><i class="bi bi-plus-circle"></i></span>
|
||||||
|
|
@ -152,4 +148,4 @@
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</app-modal-layer>
|
</div>
|
||||||
|
|
@ -5,6 +5,26 @@
|
||||||
--focus-ring: var(--pg-focus-ring, 0 0 0 3px rgba(31, 79, 214, 0.22));
|
--focus-ring: var(--pg-focus-ring, 0 0 0 3px rgba(31, 79, 214, 0.22));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.lg-backdrop {
|
||||||
|
position: fixed;
|
||||||
|
inset: 0;
|
||||||
|
background:
|
||||||
|
radial-gradient(circle at 15% 0%, rgba(31, 79, 214, 0.15), rgba(15, 23, 42, 0.66) 42%),
|
||||||
|
rgba(15, 23, 42, 0.58);
|
||||||
|
z-index: 9990;
|
||||||
|
backdrop-filter: blur(4px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.lg-modal {
|
||||||
|
position: fixed;
|
||||||
|
inset: 0;
|
||||||
|
z-index: 9995;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
.lg-modal-card {
|
.lg-modal-card {
|
||||||
width: min(1040px, 96vw);
|
width: min(1040px, 96vw);
|
||||||
max-height: 92vh;
|
max-height: 92vh;
|
||||||
|
|
@ -1,8 +1,7 @@
|
||||||
import { Component, EventEmitter, Input, OnChanges, Output, SimpleChanges } from '@angular/core';
|
import { Component, EventEmitter, Input, OnChanges, Output, SimpleChanges } from '@angular/core';
|
||||||
import { CommonModule } from '@angular/common';
|
import { CommonModule } from '@angular/common';
|
||||||
import { FormsModule } from '@angular/forms';
|
import { FormsModule } from '@angular/forms';
|
||||||
import { CustomSelectComponent } from '../../../../custom-select/custom-select';
|
import { CustomSelectComponent } from '../../../../components/custom-select/custom-select';
|
||||||
import { ModalLayerComponent } from '../../../../modal-layer/modal-layer';
|
|
||||||
|
|
||||||
export type MonthOption = { value: number; label: string };
|
export type MonthOption = { value: number; label: string };
|
||||||
|
|
||||||
|
|
@ -32,7 +31,7 @@ type PreviewRow = {
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-parcelamento-create-modal',
|
selector: 'app-parcelamento-create-modal',
|
||||||
standalone: true,
|
standalone: true,
|
||||||
imports: [CommonModule, FormsModule, CustomSelectComponent, ModalLayerComponent],
|
imports: [CommonModule, FormsModule, CustomSelectComponent],
|
||||||
templateUrl: './parcelamento-create-modal.html',
|
templateUrl: './parcelamento-create-modal.html',
|
||||||
styleUrls: ['./parcelamento-create-modal.scss'],
|
styleUrls: ['./parcelamento-create-modal.scss'],
|
||||||
})
|
})
|
||||||
|
|
@ -1,10 +1,6 @@
|
||||||
<app-modal-layer
|
<div class="lg-backdrop" *ngIf="open" (click)="close.emit()"></div>
|
||||||
[open]="open"
|
<div class="lg-modal" *ngIf="open">
|
||||||
backdropClass="lg-backdrop"
|
<div class="lg-modal-card annual-card" (click)="$event.stopPropagation()">
|
||||||
overlayClass="lg-modal"
|
|
||||||
(close)="close.emit()"
|
|
||||||
>
|
|
||||||
<div *ngIf="open" class="lg-modal-card annual-card" (click)="$event.stopPropagation()">
|
|
||||||
<div class="modal-header">
|
<div class="modal-header">
|
||||||
<div class="modal-title">
|
<div class="modal-title">
|
||||||
<span class="icon-bg"><i class="bi bi-table"></i></span>
|
<span class="icon-bg"><i class="bi bi-table"></i></span>
|
||||||
|
|
@ -57,4 +53,4 @@
|
||||||
<button class="btn-primary" type="button" (click)="close.emit()">Fechar</button>
|
<button class="btn-primary" type="button" (click)="close.emit()">Fechar</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</app-modal-layer>
|
</div>
|
||||||
|
|
@ -4,6 +4,24 @@
|
||||||
--focus-ring: 0 0 0 3px rgba(227, 61, 207, 0.16);
|
--focus-ring: 0 0 0 3px rgba(227, 61, 207, 0.16);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.lg-backdrop {
|
||||||
|
position: fixed;
|
||||||
|
inset: 0;
|
||||||
|
background: radial-gradient(circle at 20% 0%, rgba(227, 61, 207, 0.2), rgba(0, 0, 0, 0.56) 42%);
|
||||||
|
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 {
|
.lg-modal-card {
|
||||||
background: #ffffff;
|
background: #ffffff;
|
||||||
border: 1px solid rgba(255, 255, 255, 0.88);
|
border: 1px solid rgba(255, 255, 255, 0.88);
|
||||||
|
|
@ -1,7 +1,6 @@
|
||||||
import { Component, EventEmitter, Input, Output } from '@angular/core';
|
import { Component, EventEmitter, Input, Output } from '@angular/core';
|
||||||
import { CommonModule } from '@angular/common';
|
import { CommonModule } from '@angular/common';
|
||||||
import { FormsModule } from '@angular/forms';
|
import { FormsModule } from '@angular/forms';
|
||||||
import { ModalLayerComponent } from '../../../../modal-layer/modal-layer';
|
|
||||||
|
|
||||||
export type AnnualMonthValue = {
|
export type AnnualMonthValue = {
|
||||||
month: number;
|
month: number;
|
||||||
|
|
@ -21,7 +20,7 @@ export type AnnualRow = {
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-parcelamento-detalhamento-anual-modal',
|
selector: 'app-parcelamento-detalhamento-anual-modal',
|
||||||
standalone: true,
|
standalone: true,
|
||||||
imports: [CommonModule, FormsModule, ModalLayerComponent],
|
imports: [CommonModule, FormsModule],
|
||||||
templateUrl: './parcelamento-detalhamento-anual-modal.html',
|
templateUrl: './parcelamento-detalhamento-anual-modal.html',
|
||||||
styleUrls: ['./parcelamento-detalhamento-anual-modal.scss'],
|
styleUrls: ['./parcelamento-detalhamento-anual-modal.scss'],
|
||||||
})
|
})
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import { Component, EventEmitter, Input, Output } from '@angular/core';
|
import { Component, EventEmitter, Input, Output } from '@angular/core';
|
||||||
import { CommonModule } from '@angular/common';
|
import { CommonModule } from '@angular/common';
|
||||||
import { FormsModule } from '@angular/forms';
|
import { FormsModule } from '@angular/forms';
|
||||||
import { CustomSelectComponent } from '../../../../custom-select/custom-select';
|
import { CustomSelectComponent } from '../../../../components/custom-select/custom-select';
|
||||||
|
|
||||||
export type MonthOption = { value: number; label: string };
|
export type MonthOption = { value: number; label: string };
|
||||||
|
|
||||||
|
|
@ -100,7 +100,6 @@
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
*ngIf="canEdit"
|
|
||||||
class="btn-icon ghost"
|
class="btn-icon ghost"
|
||||||
type="button"
|
type="button"
|
||||||
title="Editar"
|
title="Editar"
|
||||||
|
|
@ -114,7 +113,7 @@
|
||||||
type="button"
|
type="button"
|
||||||
title="Excluir"
|
title="Excluir"
|
||||||
aria-label="Excluir"
|
aria-label="Excluir"
|
||||||
*ngIf="canDelete"
|
*ngIf="isSysAdmin"
|
||||||
(click)="remove.emit(row)">
|
(click)="remove.emit(row)">
|
||||||
<i class="bi bi-trash"></i>
|
<i class="bi bi-trash"></i>
|
||||||
</button>
|
</button>
|
||||||
|
|
@ -429,14 +429,14 @@
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
}
|
|
||||||
|
|
||||||
.page-size span {
|
span {
|
||||||
color: var(--pg-text-soft, #64748b);
|
color: var(--pg-text-soft, #64748b);
|
||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
letter-spacing: 0.05em;
|
letter-spacing: 0.05em;
|
||||||
font-weight: 800;
|
font-weight: 800;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.select-glass {
|
.select-glass {
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import { Component, EventEmitter, Input, Output } from '@angular/core';
|
import { Component, EventEmitter, Input, Output } from '@angular/core';
|
||||||
import { CommonModule } from '@angular/common';
|
import { CommonModule } from '@angular/common';
|
||||||
import { FormsModule } from '@angular/forms';
|
import { FormsModule } from '@angular/forms';
|
||||||
import { ParcelamentoListItem } from '../../../../../services/parcelamentos.service';
|
import { ParcelamentoListItem } from '../../../../services/parcelamentos.service';
|
||||||
|
|
||||||
export type ParcelamentoSegment = 'todos' | 'ativos' | 'futuros' | 'finalizados';
|
export type ParcelamentoSegment = 'todos' | 'ativos' | 'futuros' | 'finalizados';
|
||||||
|
|
||||||
|
|
@ -26,8 +26,7 @@ export class ParcelamentosTableComponent {
|
||||||
@Input() items: ParcelamentoViewItem[] = [];
|
@Input() items: ParcelamentoViewItem[] = [];
|
||||||
@Input() loading = false;
|
@Input() loading = false;
|
||||||
@Input() errorMessage = '';
|
@Input() errorMessage = '';
|
||||||
@Input() canEdit = false;
|
@Input() isSysAdmin = false;
|
||||||
@Input() canDelete = false;
|
|
||||||
|
|
||||||
@Input() segment: ParcelamentoSegment = 'todos';
|
@Input() segment: ParcelamentoSegment = 'todos';
|
||||||
@Input() segmentCounts: Record<ParcelamentoSegment, number> = {
|
@Input() segmentCounts: Record<ParcelamentoSegment, number> = {
|
||||||
|
|
@ -1,14 +1,4 @@
|
||||||
<section class="parcelamentos-page">
|
<section class="parcelamentos-page">
|
||||||
<div class="toast-container position-fixed top-0 end-0 p-3" style="z-index: 12000;">
|
|
||||||
<div class="toast border-0 shadow" [class.show]="toastOpen" [class.text-bg-success]="toastType === 'success'" [class.text-bg-danger]="toastType === 'danger'">
|
|
||||||
<div class="toast-header border-bottom-0">
|
|
||||||
<strong class="me-auto">LineGestao</strong>
|
|
||||||
<button type="button" class="btn-close" (click)="toastOpen = false"></button>
|
|
||||||
</div>
|
|
||||||
<div class="toast-body bg-white rounded-bottom text-dark fw-bold">{{ toastMessage }}</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="container-geral-responsive">
|
<div class="container-geral-responsive">
|
||||||
<div class="parcelamentos-shell">
|
<div class="parcelamentos-shell">
|
||||||
<header class="page-header">
|
<header class="page-header">
|
||||||
|
|
@ -25,11 +15,7 @@
|
||||||
<button class="btn-ghost" type="button" (click)="refresh()" [disabled]="loading">
|
<button class="btn-ghost" type="button" (click)="refresh()" [disabled]="loading">
|
||||||
<i class="bi bi-arrow-repeat"></i> Atualizar
|
<i class="bi bi-arrow-repeat"></i> Atualizar
|
||||||
</button>
|
</button>
|
||||||
<button class="btn-ghost btn-export-glass" type="button" (click)="onExport()" [disabled]="loading || exporting">
|
<button class="btn-primary" type="button" (click)="openCreateModal()">
|
||||||
<span *ngIf="!exporting"><i class="bi bi-download"></i> Exportar</span>
|
|
||||||
<span *ngIf="exporting"><span class="spinner-border spinner-border-sm me-2"></span> Exportando...</span>
|
|
||||||
</button>
|
|
||||||
<button *ngIf="canManageRecords" class="btn-primary" type="button" (click)="openCreateModal()">
|
|
||||||
<i class="bi bi-plus-circle"></i> Novo Parcelamento
|
<i class="bi bi-plus-circle"></i> Novo Parcelamento
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -79,8 +65,7 @@
|
||||||
[total]="total"
|
[total]="total"
|
||||||
[pageSize]="pageSize"
|
[pageSize]="pageSize"
|
||||||
[pageSizeOptions]="pageSizeOptions"
|
[pageSizeOptions]="pageSizeOptions"
|
||||||
[canEdit]="$any(canManageRecords)"
|
[isSysAdmin]="isSysAdmin"
|
||||||
[canDelete]="$any(isSysAdmin)"
|
|
||||||
(segmentChange)="setSegment($event)"
|
(segmentChange)="setSegment($event)"
|
||||||
(detail)="openDetails($event)"
|
(detail)="openDetails($event)"
|
||||||
(edit)="openEdit($event)"
|
(edit)="openEdit($event)"
|
||||||
|
|
@ -92,4 +77,175 @@
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<app-parcelamentos-modals [vm]="$any(vm)"></app-parcelamentos-modals>
|
<!-- Modal detalhes -->
|
||||||
|
<div class="lg-backdrop" *ngIf="detailOpen" (click)="closeDetails()"></div>
|
||||||
|
<div class="lg-modal" *ngIf="detailOpen">
|
||||||
|
<div class="lg-modal-card parcelamento-modal" (click)="$event.stopPropagation()">
|
||||||
|
<div class="modal-header">
|
||||||
|
<div class="modal-title">
|
||||||
|
<span class="icon-bg"><i class="bi bi-card-list"></i></span>
|
||||||
|
<span>Detalhes do Parcelamento</span>
|
||||||
|
</div>
|
||||||
|
<div class="modal-actions">
|
||||||
|
<button class="btn-icon" type="button" (click)="closeDetails()" aria-label="Fechar modal">
|
||||||
|
<i class="bi bi-x-lg"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="modal-body">
|
||||||
|
<div class="detail-state" *ngIf="detailLoading && !selectedDetail">
|
||||||
|
<div class="spinner-border text-brand" role="status"></div>
|
||||||
|
<span>Carregando detalhes...</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="detail-state error" *ngIf="!detailLoading && detailError && !selectedDetail">
|
||||||
|
<i class="bi bi-exclamation-triangle"></i>
|
||||||
|
<span>{{ detailError }}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<ng-container *ngIf="selectedDetail as detail">
|
||||||
|
<div class="detail-grid">
|
||||||
|
<div class="detail-card">
|
||||||
|
<small>Cliente</small>
|
||||||
|
<span class="detail-strong">{{ detail.cliente || '-' }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="detail-card">
|
||||||
|
<small>Linha</small>
|
||||||
|
<span class="detail-strong text-blue">{{ detail.linha || '-' }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="detail-card">
|
||||||
|
<small>AnoRef</small>
|
||||||
|
<span>{{ detail.anoRef ?? '-' }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="detail-card">
|
||||||
|
<small>Item</small>
|
||||||
|
<span>{{ detail.item ?? '-' }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="detail-card">
|
||||||
|
<small>Qt Parcelas</small>
|
||||||
|
<span>{{ displayQtParcelas(detail) }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="detail-card">
|
||||||
|
<small>Parcela Atual</small>
|
||||||
|
<span class="detail-strong">{{ detail.parcelaAtual ?? '-' }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="detail-card">
|
||||||
|
<small>Total Parcelas</small>
|
||||||
|
<span>{{ detail.totalParcelas ?? '-' }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="detail-card">
|
||||||
|
<small>Status</small>
|
||||||
|
<span class="status-pill">{{ detailStatus }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="detail-card">
|
||||||
|
<small>Valor Cheio</small>
|
||||||
|
<span>{{ formatMoney(detail.valorCheio) }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="detail-card">
|
||||||
|
<small>Desconto</small>
|
||||||
|
<span class="text-danger">{{ formatMoney(detail.desconto) }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="detail-card highlight">
|
||||||
|
<small>Valor com Desconto</small>
|
||||||
|
<span class="detail-strong money-strong">{{ formatMoney(detail.valorComDesconto) }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="annual-section">
|
||||||
|
<div class="annual-head">
|
||||||
|
<div class="section-title">
|
||||||
|
<i class="bi bi-table"></i>
|
||||||
|
<span>Detalhamento anual</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="annual-table-shell" *ngIf="annualRows.length > 0; else annualEmpty">
|
||||||
|
<table class="table-modern annual-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th class="sticky-col col-1">Ano</th>
|
||||||
|
<th class="sticky-col col-2 text-end">Total</th>
|
||||||
|
<th *ngFor="let m of annualMonthHeaders" class="text-end">{{ m.label }}</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr *ngFor="let row of annualRows">
|
||||||
|
<td class="sticky-col col-1">{{ row.year }}</td>
|
||||||
|
<td class="sticky-col col-2 text-end">{{ row.total | currency:'BRL':'symbol':'1.2-2':'pt-BR' }}</td>
|
||||||
|
<td *ngFor="let m of row.months" class="text-end">
|
||||||
|
{{ m.value !== null && m.value !== undefined ? (m.value | currency:'BRL':'symbol':'1.2-2':'pt-BR') : '-' }}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<ng-template #annualEmpty>
|
||||||
|
<div class="annual-empty">
|
||||||
|
Sem dados anuais.
|
||||||
|
</div>
|
||||||
|
</ng-template>
|
||||||
|
</div>
|
||||||
|
</ng-container>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button class="btn-primary" type="button" (click)="closeDetails()">Fechar</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<app-parcelamento-create-modal
|
||||||
|
[open]="createOpen"
|
||||||
|
[model]="createModel"
|
||||||
|
[monthOptions]="monthOptions"
|
||||||
|
[loading]="createSaving"
|
||||||
|
[errorMessage]="createError"
|
||||||
|
title="Novo Parcelamento"
|
||||||
|
submitLabel="Salvar"
|
||||||
|
(close)="closeCreateModal()"
|
||||||
|
(save)="saveNewParcelamento($event)">
|
||||||
|
</app-parcelamento-create-modal>
|
||||||
|
|
||||||
|
<app-parcelamento-create-modal
|
||||||
|
*ngIf="editOpen && editModel"
|
||||||
|
[open]="editOpen"
|
||||||
|
[model]="editModel"
|
||||||
|
[monthOptions]="monthOptions"
|
||||||
|
[loading]="editSaving"
|
||||||
|
[errorMessage]="editError"
|
||||||
|
title="Editar Parcelamento"
|
||||||
|
submitLabel="Atualizar"
|
||||||
|
(close)="closeEditModal()"
|
||||||
|
(save)="saveEditParcelamento($event)">
|
||||||
|
</app-parcelamento-create-modal>
|
||||||
|
|
||||||
|
<!-- Delete modal -->
|
||||||
|
<div class="lg-backdrop" *ngIf="deleteOpen"></div>
|
||||||
|
<div class="lg-modal" *ngIf="deleteOpen">
|
||||||
|
<div class="lg-modal-card modal-compact" (click)="$event.stopPropagation()">
|
||||||
|
<div class="modal-header">
|
||||||
|
<div class="modal-title">
|
||||||
|
<span class="icon-bg danger-soft"><i class="bi bi-trash"></i></span>
|
||||||
|
Remover Parcelamento
|
||||||
|
</div>
|
||||||
|
<button class="btn-icon" type="button" (click)="cancelDelete()" aria-label="Fechar modal de exclusao">
|
||||||
|
<i class="bi bi-x-lg"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<div class="confirm-delete">
|
||||||
|
<div class="confirm-icon"><i class="bi bi-trash"></i></div>
|
||||||
|
<p class="mb-0">Confirma remover o parcelamento <strong>{{ deleteTarget?.linha }}</strong>?</p>
|
||||||
|
<small class="text-danger" *ngIf="deleteError">{{ deleteError }}</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button class="btn-ghost" type="button" (click)="cancelDelete()">Cancelar</button>
|
||||||
|
<button class="btn-danger" type="button" [disabled]="deleteLoading" (click)="confirmDelete()">
|
||||||
|
{{ deleteLoading ? 'Excluindo...' : 'Excluir' }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -208,13 +208,6 @@
|
||||||
box-shadow: 0 6px 14px rgba(15, 23, 42, 0.08);
|
box-shadow: 0 6px 14px rgba(15, 23, 42, 0.08);
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-export-glass {
|
|
||||||
color: var(--pg-primary);
|
|
||||||
background: rgba(255, 255, 255, 0.68);
|
|
||||||
border-color: rgba(31, 79, 214, 0.24);
|
|
||||||
font-weight: 900;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-danger {
|
.btn-danger {
|
||||||
color: #fff;
|
color: #fff;
|
||||||
background: linear-gradient(145deg, #cf3131, #a91f1f);
|
background: linear-gradient(145deg, #cf3131, #a91f1f);
|
||||||
|
|
@ -233,10 +226,24 @@
|
||||||
color: var(--pg-primary-strong);
|
color: var(--pg-primary-strong);
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-export-glass:hover {
|
.lg-backdrop {
|
||||||
background: #fff;
|
position: fixed;
|
||||||
border-color: rgba(227, 61, 207, 0.3);
|
inset: 0;
|
||||||
color: #e33dcf;
|
background:
|
||||||
|
radial-gradient(circle at 15% 0%, rgba(31, 79, 214, 0.16), rgba(15, 23, 42, 0.64) 42%),
|
||||||
|
rgba(15, 23, 42, 0.6);
|
||||||
|
z-index: 9990;
|
||||||
|
backdrop-filter: blur(4px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.lg-modal {
|
||||||
|
position: fixed;
|
||||||
|
inset: 0;
|
||||||
|
z-index: 9995;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.lg-modal-card {
|
.lg-modal-card {
|
||||||
|
|
|
||||||
|
|
@ -2,10 +2,8 @@ import { Component, HostListener, OnDestroy, OnInit } from '@angular/core';
|
||||||
import { CommonModule } from '@angular/common';
|
import { CommonModule } from '@angular/common';
|
||||||
import { FormsModule } from '@angular/forms';
|
import { FormsModule } from '@angular/forms';
|
||||||
import { environment } from '../../../environments/environment';
|
import { environment } from '../../../environments/environment';
|
||||||
import { finalize, Subscription, firstValueFrom, timeout } from 'rxjs';
|
import { finalize, Subscription, timeout } from 'rxjs';
|
||||||
import { ParcelamentosModalsComponent } from '../../components/page-modals/parcelamento-modals/parcelamentos-modals';
|
|
||||||
import { AuthService } from '../../services/auth.service';
|
import { AuthService } from '../../services/auth.service';
|
||||||
import { TableExportService } from '../../services/table-export.service';
|
|
||||||
import {
|
import {
|
||||||
ParcelamentosService,
|
ParcelamentosService,
|
||||||
ParcelamentoListItem,
|
ParcelamentoListItem,
|
||||||
|
|
@ -20,35 +18,27 @@ import {
|
||||||
import {
|
import {
|
||||||
ParcelamentosKpisComponent,
|
ParcelamentosKpisComponent,
|
||||||
ParcelamentoKpi,
|
ParcelamentoKpi,
|
||||||
} from '../../components/page-modals/parcelamento-modals/components/parcelamentos-kpis/parcelamentos-kpis';
|
} from './components/parcelamentos-kpis/parcelamentos-kpis';
|
||||||
import {
|
import {
|
||||||
ParcelamentosFiltersComponent,
|
ParcelamentosFiltersComponent,
|
||||||
ParcelamentosFiltersModel,
|
ParcelamentosFiltersModel,
|
||||||
FilterChip,
|
FilterChip,
|
||||||
} from '../../components/page-modals/parcelamento-modals/components/parcelamentos-filters/parcelamentos-filters';
|
} from './components/parcelamentos-filters/parcelamentos-filters';
|
||||||
import {
|
import {
|
||||||
ParcelamentosTableComponent,
|
ParcelamentosTableComponent,
|
||||||
ParcelamentoSegment,
|
ParcelamentoSegment,
|
||||||
ParcelamentoViewItem,
|
ParcelamentoViewItem,
|
||||||
} from '../../components/page-modals/parcelamento-modals/components/parcelamentos-table/parcelamentos-table';
|
} from './components/parcelamentos-table/parcelamentos-table';
|
||||||
import {
|
import {
|
||||||
|
ParcelamentoCreateModalComponent,
|
||||||
ParcelamentoCreateModel,
|
ParcelamentoCreateModel,
|
||||||
} from '../../components/page-modals/parcelamento-modals/components/parcelamento-create-modal/parcelamento-create-modal';
|
} from './components/parcelamento-create-modal/parcelamento-create-modal';
|
||||||
import { confirmDeletionWithTyping } from '../../utils/destructive-confirmation';
|
import { confirmDeletionWithTyping } from '../../utils/destructive-confirmation';
|
||||||
import {
|
|
||||||
buildPageNumbers,
|
|
||||||
clampPage,
|
|
||||||
computePageEnd,
|
|
||||||
computePageStart,
|
|
||||||
computeTotalPages
|
|
||||||
} from '../../utils/pagination.util';
|
|
||||||
import { normalizeAccentInsensitive } from '../../utils/text-normalization.util';
|
|
||||||
|
|
||||||
type MonthOption = { value: number; label: string };
|
type MonthOption = { value: number; label: string };
|
||||||
type ParcelamentoStatus = 'ativos' | 'futuros' | 'finalizados';
|
type ParcelamentoStatus = 'ativos' | 'futuros' | 'finalizados';
|
||||||
type AnnualMonthValue = { month: number; label: string; value: number | null };
|
type AnnualMonthValue = { month: number; label: string; value: number | null };
|
||||||
type AnnualRow = { year: number; total: number; months: AnnualMonthValue[] };
|
type AnnualRow = { year: number; total: number; months: AnnualMonthValue[] };
|
||||||
type ParcelamentoExportRow = ParcelamentoViewItem & Partial<ParcelamentoDetail>;
|
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-parcelamentos',
|
selector: 'app-parcelamentos',
|
||||||
|
|
@ -56,23 +46,17 @@ type ParcelamentoExportRow = ParcelamentoViewItem & Partial<ParcelamentoDetail>;
|
||||||
imports: [
|
imports: [
|
||||||
CommonModule,
|
CommonModule,
|
||||||
FormsModule,
|
FormsModule,
|
||||||
ParcelamentosModalsComponent,
|
|
||||||
ParcelamentosKpisComponent,
|
ParcelamentosKpisComponent,
|
||||||
ParcelamentosFiltersComponent,
|
ParcelamentosFiltersComponent,
|
||||||
ParcelamentosTableComponent,
|
ParcelamentosTableComponent,
|
||||||
|
ParcelamentoCreateModalComponent,
|
||||||
],
|
],
|
||||||
templateUrl: './parcelamentos.html',
|
templateUrl: './parcelamentos.html',
|
||||||
styleUrls: ['./parcelamentos.scss'],
|
styleUrls: ['./parcelamentos.scss'],
|
||||||
})
|
})
|
||||||
export class Parcelamentos implements OnInit, OnDestroy {
|
export class Parcelamentos implements OnInit, OnDestroy {
|
||||||
readonly vm = this;
|
|
||||||
loading = false;
|
loading = false;
|
||||||
exporting = false;
|
|
||||||
errorMessage = '';
|
errorMessage = '';
|
||||||
toastOpen = false;
|
|
||||||
toastMessage = '';
|
|
||||||
toastType: 'success' | 'danger' = 'success';
|
|
||||||
private toastTimer: ReturnType<typeof setTimeout> | null = null;
|
|
||||||
|
|
||||||
debugMode = !environment.production;
|
debugMode = !environment.production;
|
||||||
|
|
||||||
|
|
@ -104,12 +88,6 @@ export class Parcelamentos implements OnInit, OnDestroy {
|
||||||
activeChips: FilterChip[] = [];
|
activeChips: FilterChip[] = [];
|
||||||
|
|
||||||
isSysAdmin = false;
|
isSysAdmin = false;
|
||||||
isGestor = false;
|
|
||||||
isFinanceiro = false;
|
|
||||||
|
|
||||||
get canManageRecords(): boolean {
|
|
||||||
return this.isSysAdmin || this.isGestor;
|
|
||||||
}
|
|
||||||
|
|
||||||
detailOpen = false;
|
detailOpen = false;
|
||||||
detailLoading = false;
|
detailLoading = false;
|
||||||
|
|
@ -159,8 +137,7 @@ export class Parcelamentos implements OnInit, OnDestroy {
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private parcelamentosService: ParcelamentosService,
|
private parcelamentosService: ParcelamentosService,
|
||||||
private authService: AuthService,
|
private authService: AuthService
|
||||||
private tableExportService: TableExportService
|
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
|
|
@ -170,7 +147,6 @@ export class Parcelamentos implements OnInit, OnDestroy {
|
||||||
|
|
||||||
ngOnDestroy(): void {
|
ngOnDestroy(): void {
|
||||||
this.cancelDetailRequest();
|
this.cancelDetailRequest();
|
||||||
if (this.toastTimer) clearTimeout(this.toastTimer);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@HostListener('document:keydown.escape')
|
@HostListener('document:keydown.escape')
|
||||||
|
|
@ -183,24 +159,30 @@ export class Parcelamentos implements OnInit, OnDestroy {
|
||||||
|
|
||||||
private syncPermissions(): void {
|
private syncPermissions(): void {
|
||||||
this.isSysAdmin = this.authService.hasRole('sysadmin');
|
this.isSysAdmin = this.authService.hasRole('sysadmin');
|
||||||
this.isGestor = this.authService.hasRole('gestor');
|
|
||||||
this.isFinanceiro = this.authService.hasRole('financeiro');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
get totalPages(): number {
|
get totalPages(): number {
|
||||||
return computeTotalPages(this.total || 0, this.pageSize || 10);
|
return Math.max(1, Math.ceil((this.total || 0) / (this.pageSize || 10)));
|
||||||
}
|
}
|
||||||
|
|
||||||
get pageNumbers(): number[] {
|
get pageNumbers(): number[] {
|
||||||
return buildPageNumbers(this.page, this.totalPages);
|
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++) pages.push(i);
|
||||||
|
return pages;
|
||||||
}
|
}
|
||||||
|
|
||||||
get pageStart(): number {
|
get pageStart(): number {
|
||||||
return computePageStart(this.total || 0, this.page, this.pageSize);
|
return this.total === 0 ? 0 : (this.page - 1) * this.pageSize + 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
get pageEnd(): number {
|
get pageEnd(): number {
|
||||||
return computePageEnd(this.total || 0, this.page, this.pageSize);
|
return this.total === 0 ? 0 : Math.min(this.page * this.pageSize, this.total);
|
||||||
}
|
}
|
||||||
|
|
||||||
get competenciaInvalid(): boolean {
|
get competenciaInvalid(): boolean {
|
||||||
|
|
@ -291,50 +273,6 @@ export class Parcelamentos implements OnInit, OnDestroy {
|
||||||
this.load();
|
this.load();
|
||||||
}
|
}
|
||||||
|
|
||||||
async onExport(): Promise<void> {
|
|
||||||
if (this.exporting) return;
|
|
||||||
this.exporting = true;
|
|
||||||
|
|
||||||
try {
|
|
||||||
const baseRows = await this.fetchAllItemsForExport();
|
|
||||||
const rows = await this.fetchDetailedItemsForExport(baseRows);
|
|
||||||
if (!rows.length) {
|
|
||||||
this.showToast('Nenhum registro encontrado para exportar.', 'danger');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const timestamp = this.tableExportService.buildTimestamp();
|
|
||||||
await this.tableExportService.exportAsXlsx<ParcelamentoExportRow>({
|
|
||||||
fileName: `parcelamentos_${this.activeSegment}_${timestamp}`,
|
|
||||||
sheetName: 'Parcelamentos',
|
|
||||||
rows,
|
|
||||||
columns: [
|
|
||||||
{ header: 'ID', value: (row) => row.id ?? '' },
|
|
||||||
{ header: 'Ano Ref', type: 'number', value: (row) => this.toNumber(row.anoRef) ?? 0 },
|
|
||||||
{ header: 'Item', type: 'number', value: (row) => this.toNumber(row.item) ?? 0 },
|
|
||||||
{ header: 'Linha', value: (row) => row.linha ?? '' },
|
|
||||||
{ header: 'Cliente', value: (row) => row.cliente ?? '' },
|
|
||||||
{ header: 'Status', value: (row) => row.statusLabel },
|
|
||||||
{ header: 'Parcela Atual', type: 'number', value: (row) => this.toNumber(row.parcelaAtual) ?? 0 },
|
|
||||||
{ header: 'Total Parcelas', type: 'number', value: (row) => this.toNumber(row.totalParcelas) ?? 0 },
|
|
||||||
{ header: 'Qt Parcelas', value: (row) => row.qtParcelas ?? '' },
|
|
||||||
{ header: 'Valor Cheio', type: 'currency', value: (row) => this.toNumber(row.valorCheio) ?? 0 },
|
|
||||||
{ header: 'Desconto', type: 'currency', value: (row) => this.toNumber(row.desconto) ?? 0 },
|
|
||||||
{ header: 'Valor c/ Desconto', type: 'currency', value: (row) => this.toNumber(row.valorComDesconto) ?? 0 },
|
|
||||||
{ header: 'Valor Parcela', type: 'currency', value: (row) => this.toNumber(row.valorParcela) ?? 0 },
|
|
||||||
{ header: 'Parcelas Mensais', value: (row) => this.stringifyParcelasMensais(row.parcelasMensais) },
|
|
||||||
{ header: 'Detalhamento Anual', value: (row) => this.stringifyAnnualRows(row.annualRows) },
|
|
||||||
],
|
|
||||||
});
|
|
||||||
|
|
||||||
this.showToast(`Planilha exportada com ${rows.length} registro(s).`, 'success');
|
|
||||||
} catch {
|
|
||||||
this.showToast('Erro ao exportar planilha.', 'danger');
|
|
||||||
} finally {
|
|
||||||
this.exporting = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onPageSizeChange(size: number): void {
|
onPageSizeChange(size: number): void {
|
||||||
this.pageSize = size;
|
this.pageSize = size;
|
||||||
this.page = 1;
|
this.page = 1;
|
||||||
|
|
@ -342,7 +280,7 @@ export class Parcelamentos implements OnInit, OnDestroy {
|
||||||
}
|
}
|
||||||
|
|
||||||
goToPage(p: number): void {
|
goToPage(p: number): void {
|
||||||
this.page = clampPage(p, this.totalPages);
|
this.page = Math.max(1, Math.min(this.totalPages, p));
|
||||||
this.load();
|
this.load();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -418,11 +356,6 @@ export class Parcelamentos implements OnInit, OnDestroy {
|
||||||
}
|
}
|
||||||
|
|
||||||
openCreateModal(): void {
|
openCreateModal(): void {
|
||||||
if (!this.canManageRecords) {
|
|
||||||
this.showToast('Perfil Financeiro possui acesso somente leitura.', 'danger');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.createModel = this.buildCreateModel();
|
this.createModel = this.buildCreateModel();
|
||||||
this.createError = '';
|
this.createError = '';
|
||||||
this.createOpen = true;
|
this.createOpen = true;
|
||||||
|
|
@ -435,11 +368,6 @@ export class Parcelamentos implements OnInit, OnDestroy {
|
||||||
}
|
}
|
||||||
|
|
||||||
saveNewParcelamento(model: ParcelamentoCreateModel): void {
|
saveNewParcelamento(model: ParcelamentoCreateModel): void {
|
||||||
if (!this.canManageRecords) {
|
|
||||||
this.showToast('Perfil Financeiro possui acesso somente leitura.', 'danger');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.createSaving) return;
|
if (this.createSaving) return;
|
||||||
this.createSaving = true;
|
this.createSaving = true;
|
||||||
this.createError = '';
|
this.createError = '';
|
||||||
|
|
@ -458,11 +386,6 @@ export class Parcelamentos implements OnInit, OnDestroy {
|
||||||
}
|
}
|
||||||
|
|
||||||
openEdit(item: ParcelamentoListItem): void {
|
openEdit(item: ParcelamentoListItem): void {
|
||||||
if (!this.canManageRecords) {
|
|
||||||
this.showToast('Perfil Financeiro possui acesso somente leitura.', 'danger');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const id = this.getItemId(item);
|
const id = this.getItemId(item);
|
||||||
if (!id) return;
|
if (!id) return;
|
||||||
this.editOpen = true;
|
this.editOpen = true;
|
||||||
|
|
@ -498,11 +421,6 @@ export class Parcelamentos implements OnInit, OnDestroy {
|
||||||
}
|
}
|
||||||
|
|
||||||
saveEditParcelamento(model: ParcelamentoCreateModel): void {
|
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;
|
if (this.editSaving || !this.editModel || !this.editId) return;
|
||||||
this.editSaving = true;
|
this.editSaving = true;
|
||||||
this.editError = '';
|
this.editError = '';
|
||||||
|
|
@ -751,7 +669,7 @@ export class Parcelamentos implements OnInit, OnDestroy {
|
||||||
}
|
}
|
||||||
|
|
||||||
private applySearch(list: ParcelamentoViewItem[], term: string): ParcelamentoViewItem[] {
|
private applySearch(list: ParcelamentoViewItem[], term: string): ParcelamentoViewItem[] {
|
||||||
const search = normalizeAccentInsensitive(term);
|
const search = this.normalizeText(term);
|
||||||
if (!search) return list;
|
if (!search) return list;
|
||||||
return list.filter((item) => {
|
return list.filter((item) => {
|
||||||
const payload = [
|
const payload = [
|
||||||
|
|
@ -763,129 +681,10 @@ export class Parcelamentos implements OnInit, OnDestroy {
|
||||||
]
|
]
|
||||||
.map((v) => (v ?? '').toString())
|
.map((v) => (v ?? '').toString())
|
||||||
.join(' ');
|
.join(' ');
|
||||||
return normalizeAccentInsensitive(payload).includes(search);
|
return this.normalizeText(payload).includes(search);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private async fetchAllItemsForExport(): Promise<ParcelamentoViewItem[]> {
|
|
||||||
const anoRef = this.parseNumber(this.filters.anoRef);
|
|
||||||
const competenciaAno = this.parseNumber(this.filters.competenciaAno);
|
|
||||||
const competenciaMes = this.parseNumber(this.filters.competenciaMes);
|
|
||||||
const sendCompetencia = competenciaAno !== null && competenciaMes !== null;
|
|
||||||
|
|
||||||
const pageSize = 500;
|
|
||||||
let page = 1;
|
|
||||||
let expectedTotal = 0;
|
|
||||||
const allItems: ParcelamentoListItem[] = [];
|
|
||||||
|
|
||||||
while (page <= 500) {
|
|
||||||
const response = await firstValueFrom(
|
|
||||||
this.parcelamentosService.list({
|
|
||||||
anoRef: anoRef ?? undefined,
|
|
||||||
linha: this.filters.linha?.trim() || undefined,
|
|
||||||
cliente: this.filters.cliente?.trim() || undefined,
|
|
||||||
competenciaAno: sendCompetencia ? competenciaAno ?? undefined : undefined,
|
|
||||||
competenciaMes: sendCompetencia ? competenciaMes ?? undefined : undefined,
|
|
||||||
page,
|
|
||||||
pageSize,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
const normalized = this.normalizeListResponse(response);
|
|
||||||
allItems.push(...normalized.items);
|
|
||||||
expectedTotal = normalized.total;
|
|
||||||
|
|
||||||
if (normalized.items.length === 0) break;
|
|
||||||
if (normalized.items.length < pageSize) break;
|
|
||||||
if (expectedTotal > 0 && allItems.length >= expectedTotal) break;
|
|
||||||
page += 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
const base = allItems.map((item) => this.toViewItem(item));
|
|
||||||
const searched = this.applySearch(base, this.filters.search);
|
|
||||||
return this.activeSegment === 'todos'
|
|
||||||
? searched
|
|
||||||
: searched.filter((item) => item.status === this.activeSegment);
|
|
||||||
}
|
|
||||||
|
|
||||||
private async fetchDetailedItemsForExport(rows: ParcelamentoViewItem[]): Promise<ParcelamentoExportRow[]> {
|
|
||||||
if (!rows.length) return [];
|
|
||||||
|
|
||||||
const detailedRows: ParcelamentoExportRow[] = [];
|
|
||||||
const chunkSize = 10;
|
|
||||||
|
|
||||||
for (let i = 0; i < rows.length; i += chunkSize) {
|
|
||||||
const chunk = rows.slice(i, i + chunkSize);
|
|
||||||
const resolved = await Promise.all(
|
|
||||||
chunk.map(async (row) => {
|
|
||||||
const id = this.getItemId(row);
|
|
||||||
if (!id) return row;
|
|
||||||
try {
|
|
||||||
const detailRes = await firstValueFrom(this.parcelamentosService.getById(id));
|
|
||||||
const detail = this.normalizeDetail(detailRes);
|
|
||||||
return {
|
|
||||||
...row,
|
|
||||||
...detail,
|
|
||||||
};
|
|
||||||
} catch {
|
|
||||||
return row;
|
|
||||||
}
|
|
||||||
})
|
|
||||||
);
|
|
||||||
detailedRows.push(...resolved);
|
|
||||||
}
|
|
||||||
|
|
||||||
return detailedRows;
|
|
||||||
}
|
|
||||||
|
|
||||||
private stringifyParcelasMensais(parcelas?: ParcelamentoParcela[] | null): string {
|
|
||||||
if (!parcelas?.length) return '';
|
|
||||||
return parcelas
|
|
||||||
.map((parcela) => {
|
|
||||||
const competencia = (parcela.competencia ?? '').toString().trim();
|
|
||||||
const valor = this.toNumber(parcela.valor);
|
|
||||||
const valorFmt = valor === null ? '-' : new Intl.NumberFormat('pt-BR', { style: 'currency', currency: 'BRL' }).format(valor);
|
|
||||||
return `${competencia || '-'}: ${valorFmt}`;
|
|
||||||
})
|
|
||||||
.join(' | ');
|
|
||||||
}
|
|
||||||
|
|
||||||
private stringifyAnnualRows(rows?: ParcelamentoAnnualRow[] | null): string {
|
|
||||||
if (!rows?.length) return '';
|
|
||||||
return rows
|
|
||||||
.map((row) => {
|
|
||||||
const year = this.parseNumber(row.year);
|
|
||||||
const total = this.toNumber(row.total);
|
|
||||||
const totalFmt = total === null ? '-' : new Intl.NumberFormat('pt-BR', { style: 'currency', currency: 'BRL' }).format(total);
|
|
||||||
|
|
||||||
const months = (row.months ?? [])
|
|
||||||
.map((month) => {
|
|
||||||
const monthNum = this.parseNumber(month.month);
|
|
||||||
const monthValue = this.toNumber(month.valor);
|
|
||||||
const monthLabel = monthNum ? String(monthNum).padStart(2, '0') : '--';
|
|
||||||
const monthFmt = monthValue === null ? '-' : new Intl.NumberFormat('pt-BR', { style: 'currency', currency: 'BRL' }).format(monthValue);
|
|
||||||
return `${monthLabel}:${monthFmt}`;
|
|
||||||
})
|
|
||||||
.join(', ');
|
|
||||||
|
|
||||||
return `${year ?? '----'} (Total ${totalFmt})${months ? ` [${months}]` : ''}`;
|
|
||||||
})
|
|
||||||
.join(' | ');
|
|
||||||
}
|
|
||||||
|
|
||||||
private normalizeListResponse(response: any): { items: ParcelamentoListItem[]; total: number } {
|
|
||||||
const anyRes: any = response ?? {};
|
|
||||||
const items = Array.isArray(anyRes.items)
|
|
||||||
? anyRes.items.filter(Boolean)
|
|
||||||
: Array.isArray(anyRes.Items)
|
|
||||||
? anyRes.Items.filter(Boolean)
|
|
||||||
: [];
|
|
||||||
const total = typeof anyRes.total === 'number'
|
|
||||||
? anyRes.total
|
|
||||||
: (typeof anyRes.Total === 'number' ? anyRes.Total : 0);
|
|
||||||
return { items, total };
|
|
||||||
}
|
|
||||||
|
|
||||||
private resolveStatus(item: ParcelamentoListItem): ParcelamentoStatus {
|
private resolveStatus(item: ParcelamentoListItem): ParcelamentoStatus {
|
||||||
const total = this.toNumber(item.totalParcelas);
|
const total = this.toNumber(item.totalParcelas);
|
||||||
const atual = this.toNumber(item.parcelaAtual);
|
const atual = this.toNumber(item.parcelaAtual);
|
||||||
|
|
@ -1183,12 +982,13 @@ export class Parcelamentos implements OnInit, OnDestroy {
|
||||||
return Number.isNaN(n) ? null : n;
|
return Number.isNaN(n) ? null : n;
|
||||||
}
|
}
|
||||||
|
|
||||||
private showToast(message: string, type: 'success' | 'danger'): void {
|
private normalizeText(value: any): string {
|
||||||
this.toastMessage = message;
|
return (value ?? '')
|
||||||
this.toastType = type;
|
.toString()
|
||||||
this.toastOpen = true;
|
.trim()
|
||||||
if (this.toastTimer) clearTimeout(this.toastTimer);
|
.toUpperCase()
|
||||||
this.toastTimer = setTimeout(() => (this.toastOpen = false), 3000);
|
.normalize('NFD')
|
||||||
|
.replace(/[\u0300-\u036f]/g, '');
|
||||||
}
|
}
|
||||||
|
|
||||||
private onlyDigits(value: string): string {
|
private onlyDigits(value: string): string {
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,4 @@
|
||||||
<section class="resumo-page">
|
<section class="resumo-page">
|
||||||
<div class="toast-container position-fixed top-0 end-0 p-3" style="z-index: 12000;">
|
|
||||||
<div class="toast border-0 shadow" [class.show]="toastOpen" [class.text-bg-success]="toastType === 'success'" [class.text-bg-danger]="toastType === 'danger'">
|
|
||||||
<div class="toast-header border-bottom-0">
|
|
||||||
<strong class="me-auto">LineGestao</strong>
|
|
||||||
<button type="button" class="btn-close" (click)="toastOpen = false"></button>
|
|
||||||
</div>
|
|
||||||
<div class="toast-body bg-white rounded-bottom text-dark fw-bold">{{ toastMessage }}</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="wrap">
|
<div class="wrap">
|
||||||
<div class="resumo-container">
|
<div class="resumo-container">
|
||||||
|
|
||||||
|
|
@ -128,9 +118,9 @@
|
||||||
<i class="bi" [class.bi-arrows-angle-expand]="macrophonyCompact" [class.bi-arrows-collapse]="!macrophonyCompact"></i>
|
<i class="bi" [class.bi-arrows-angle-expand]="macrophonyCompact" [class.bi-arrows-collapse]="!macrophonyCompact"></i>
|
||||||
<span class="hide-mobile">{{ macrophonyCompact ? 'Expandir' : 'Compactar' }}</span>
|
<span class="hide-mobile">{{ macrophonyCompact ? 'Expandir' : 'Compactar' }}</span>
|
||||||
</button>
|
</button>
|
||||||
<button class="btn-icon-text btn-export-glass" type="button" (click)="exportMacrophonyCsv()" [disabled]="isExporting('macrophony-planos')">
|
<button class="btn-icon-text" type="button" (click)="exportMacrophonyCsv()">
|
||||||
<i class="bi" [class.bi-download]="!isExporting('macrophony-planos')" [class.bi-hourglass-split]="isExporting('macrophony-planos')"></i>
|
<i class="bi bi-download"></i>
|
||||||
<span class="hide-mobile">{{ isExporting('macrophony-planos') ? 'Exportando...' : 'Exportar' }}</span>
|
<span class="hide-mobile">Exportar</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -447,9 +437,9 @@
|
||||||
<i class="bi" [class.bi-arrows-angle-expand]="group.compact" [class.bi-arrows-collapse]="!group.compact"></i>
|
<i class="bi" [class.bi-arrows-angle-expand]="group.compact" [class.bi-arrows-collapse]="!group.compact"></i>
|
||||||
<span class="hide-mobile">{{ group.compact ? 'Expandir' : 'Compactar' }}</span>
|
<span class="hide-mobile">{{ group.compact ? 'Expandir' : 'Compactar' }}</span>
|
||||||
</button>
|
</button>
|
||||||
<button class="btn-icon-text btn-export-glass" type="button" (click)="exportGroupedCsv(group, file)" [disabled]="isExporting(file)">
|
<button class="btn-icon-text" type="button" (click)="exportGroupedCsv(group, file)">
|
||||||
<i class="bi" [class.bi-download]="!isExporting(file)" [class.bi-hourglass-split]="isExporting(file)"></i>
|
<i class="bi bi-download"></i>
|
||||||
<span class="hide-mobile">{{ isExporting(file) ? 'Exportando...' : 'Exportar' }}</span>
|
<span class="hide-mobile">Exportar</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -190,19 +190,6 @@
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-export-glass {
|
|
||||||
color: var(--blue);
|
|
||||||
border-color: rgba(3, 15, 170, 0.22);
|
|
||||||
background: rgba(255, 255, 255, 0.72);
|
|
||||||
font-weight: 800;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-export-glass:hover:not(:disabled) {
|
|
||||||
border-color: var(--brand);
|
|
||||||
color: var(--brand);
|
|
||||||
background: #fff;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-icon {
|
.btn-icon {
|
||||||
width: 32px;
|
width: 32px;
|
||||||
height: 32px;
|
height: 32px;
|
||||||
|
|
|
||||||
|
|
@ -31,17 +31,7 @@ import {
|
||||||
ReservaPorDdd,
|
ReservaPorDdd,
|
||||||
ReservaTotal
|
ReservaTotal
|
||||||
} from '../../services/resumo.service';
|
} from '../../services/resumo.service';
|
||||||
import { TableExportService, type ExportCellType } from '../../services/table-export.service';
|
|
||||||
import { environment } from '../../../environments/environment';
|
import { environment } from '../../../environments/environment';
|
||||||
import {
|
|
||||||
buildPageNumbers,
|
|
||||||
clampPage,
|
|
||||||
computePageEnd,
|
|
||||||
computePageStart,
|
|
||||||
computeTotalPages
|
|
||||||
} from '../../utils/pagination.util';
|
|
||||||
import { normalizeAccentInsensitive } from '../../utils/text-normalization.util';
|
|
||||||
import { buildApiBaseUrl } from '../../utils/api-base.util';
|
|
||||||
|
|
||||||
type ResumoTab = 'planos' | 'clientes' | 'totais' | 'reserva';
|
type ResumoTab = 'planos' | 'clientes' | 'totais' | 'reserva';
|
||||||
|
|
||||||
|
|
@ -95,11 +85,6 @@ export class Resumo implements OnInit, AfterViewInit, OnDestroy {
|
||||||
|
|
||||||
loading = false;
|
loading = false;
|
||||||
errorMessage = '';
|
errorMessage = '';
|
||||||
toastOpen = false;
|
|
||||||
toastMessage = '';
|
|
||||||
toastType: 'success' | 'danger' = 'success';
|
|
||||||
private toastTimer: ReturnType<typeof setTimeout> | null = null;
|
|
||||||
private exportingKeys = new Set<string>();
|
|
||||||
resumo: ResumoResponse | null = null;
|
resumo: ResumoResponse | null = null;
|
||||||
|
|
||||||
activeTab: ResumoTab = 'planos';
|
activeTab: ResumoTab = 'planos';
|
||||||
|
|
@ -154,10 +139,10 @@ export class Resumo implements OnInit, AfterViewInit, OnDestroy {
|
||||||
private resumoService: ResumoService,
|
private resumoService: ResumoService,
|
||||||
private route: ActivatedRoute,
|
private route: ActivatedRoute,
|
||||||
private router: Router,
|
private router: Router,
|
||||||
private cdr: ChangeDetectorRef,
|
private cdr: ChangeDetectorRef
|
||||||
private tableExportService: TableExportService
|
|
||||||
) {
|
) {
|
||||||
this.baseApi = buildApiBaseUrl(environment.apiUrl);
|
const raw = (environment.apiUrl || '').replace(/\/+$/, '');
|
||||||
|
this.baseApi = raw.toLowerCase().endsWith('/api') ? raw : `${raw}/api`;
|
||||||
this.initTables();
|
this.initTables();
|
||||||
this.initGroupTables();
|
this.initGroupTables();
|
||||||
// Default chart configuration for Enterprise look
|
// Default chart configuration for Enterprise look
|
||||||
|
|
@ -187,7 +172,6 @@ export class Resumo implements OnInit, AfterViewInit, OnDestroy {
|
||||||
|
|
||||||
ngOnDestroy(): void {
|
ngOnDestroy(): void {
|
||||||
Object.values(this.charts).forEach(c => c?.destroy());
|
Object.values(this.charts).forEach(c => c?.destroy());
|
||||||
if (this.toastTimer) clearTimeout(this.toastTimer);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
setTab(tab: ResumoTab): void {
|
setTab(tab: ResumoTab): void {
|
||||||
|
|
@ -652,7 +636,7 @@ export class Resumo implements OnInit, AfterViewInit, OnDestroy {
|
||||||
toggleMacrophonyGroup(key: string) { if (this.macrophonyOpen.has(key)) this.macrophonyOpen.delete(key); else this.macrophonyOpen.add(key); }
|
toggleMacrophonyGroup(key: string) { if (this.macrophonyOpen.has(key)) this.macrophonyOpen.delete(key); else this.macrophonyOpen.add(key); }
|
||||||
openMacrophonyDetail(g: MacrophonyGroup) { this.macrophonyDetailGroup = g; this.macrophonyDetailOpen = true; }
|
openMacrophonyDetail(g: MacrophonyGroup) { this.macrophonyDetailGroup = g; this.macrophonyDetailOpen = true; }
|
||||||
closeMacrophonyDetail() { this.macrophonyDetailOpen = false; this.macrophonyDetailGroup = null; }
|
closeMacrophonyDetail() { this.macrophonyDetailOpen = false; this.macrophonyDetailGroup = null; }
|
||||||
goToMacrophonyPage(p: number) { this.macrophonyPage = clampPage(p, this.macrophonyTotalPages); this.updateMacrophonyView(); }
|
goToMacrophonyPage(p: number) { this.macrophonyPage = p; this.updateMacrophonyView(); }
|
||||||
|
|
||||||
onGroupedSearch<T>(g: GroupedTableState<T>, value?: string) {
|
onGroupedSearch<T>(g: GroupedTableState<T>, value?: string) {
|
||||||
if (typeof value === 'string') g.search = value;
|
if (typeof value === 'string') g.search = value;
|
||||||
|
|
@ -660,19 +644,25 @@ export class Resumo implements OnInit, AfterViewInit, OnDestroy {
|
||||||
this.updateGroupView(g);
|
this.updateGroupView(g);
|
||||||
}
|
}
|
||||||
toggleGroupedCompact<T>(g: GroupedTableState<T>) { g.compact = !g.compact; }
|
toggleGroupedCompact<T>(g: GroupedTableState<T>) { g.compact = !g.compact; }
|
||||||
exportGroupedCsv<T>(g: GroupedTableState<T>, file: string) { void this.exportTableAsXlsx(g.table, file); }
|
exportGroupedCsv<T>(g: GroupedTableState<T>, file: string) { this.exportCsv(g.table, file); }
|
||||||
isGroupedOpen<T>(g: GroupedTableState<T>, key: string) { return g.open.has(key); }
|
isGroupedOpen<T>(g: GroupedTableState<T>, key: string) { return g.open.has(key); }
|
||||||
toggleGroupedOpen<T>(g: GroupedTableState<T>, key: string) { if (g.open.has(key)) g.open.delete(key); else g.open.add(key); }
|
toggleGroupedOpen<T>(g: GroupedTableState<T>, key: string) { if (g.open.has(key)) g.open.delete(key); else g.open.add(key); }
|
||||||
openGroupedDetail<T>(g: GroupedTableState<T>, item: GroupItem<T>) { g.detailGroup = item; g.detailOpen = true; }
|
openGroupedDetail<T>(g: GroupedTableState<T>, item: GroupItem<T>) { g.detailGroup = item; g.detailOpen = true; }
|
||||||
closeGroupedDetail<T>(g: GroupedTableState<T>) { g.detailOpen = false; g.detailGroup = null; }
|
closeGroupedDetail<T>(g: GroupedTableState<T>) { g.detailOpen = false; g.detailGroup = null; }
|
||||||
getGroupedPageStart<T>(g: GroupedTableState<T>) { return computePageStart(g.filtered.length, g.page, g.pageSize); }
|
getGroupedPageStart<T>(g: GroupedTableState<T>) { return g.filtered.length ? ((g.page - 1) * g.pageSize + 1) : 0; }
|
||||||
getGroupedPageEnd<T>(g: GroupedTableState<T>) { return computePageEnd(g.filtered.length, g.page, g.pageSize); }
|
getGroupedPageEnd<T>(g: GroupedTableState<T>) { return g.filtered.length ? Math.min(g.page * g.pageSize, g.filtered.length) : 0; }
|
||||||
getGroupedPageNumbers<T>(g: GroupedTableState<T>) {
|
getGroupedPageNumbers<T>(g: GroupedTableState<T>) {
|
||||||
return buildPageNumbers(g.page, this.getGroupedTotalPages(g));
|
const total = this.getGroupedTotalPages(g);
|
||||||
|
if (total <= 1) return [1];
|
||||||
|
const current = Math.min(Math.max(g.page, 1), total);
|
||||||
|
const start = Math.max(1, current - 2);
|
||||||
|
const end = Math.min(total, start + 4);
|
||||||
|
const adjustedStart = Math.max(1, end - 4);
|
||||||
|
return Array.from({ length: end - adjustedStart + 1 }, (_, i) => adjustedStart + i);
|
||||||
}
|
}
|
||||||
getGroupedTotalPages<T>(g: GroupedTableState<T>) { return computeTotalPages(g.filtered.length, g.pageSize); }
|
getGroupedTotalPages<T>(g: GroupedTableState<T>) { return Math.max(1, Math.ceil(g.filtered.length / g.pageSize)); }
|
||||||
goToGroupedPage<T>(g: GroupedTableState<T>, p: number) {
|
goToGroupedPage<T>(g: GroupedTableState<T>, p: number) {
|
||||||
g.page = clampPage(p, this.getGroupedTotalPages(g));
|
g.page = Math.min(this.getGroupedTotalPages(g), Math.max(1, p));
|
||||||
this.updateGroupView(g);
|
this.updateGroupView(g);
|
||||||
}
|
}
|
||||||
getTableRowClass<T>(_: TableState<T>, __: T) { return false; }
|
getTableRowClass<T>(_: TableState<T>, __: T) { return false; }
|
||||||
|
|
@ -687,10 +677,6 @@ export class Resumo implements OnInit, AfterViewInit, OnDestroy {
|
||||||
return normalized === 'true' || normalized === '1' || normalized === 'sim';
|
return normalized === 'true' || normalized === '1' || normalized === 'sim';
|
||||||
}
|
}
|
||||||
|
|
||||||
isExporting(key: string): boolean {
|
|
||||||
return this.exportingKeys.has(key);
|
|
||||||
}
|
|
||||||
|
|
||||||
private initTables() {
|
private initTables() {
|
||||||
const hideMoneyColumns = <T>(cols: TableColumn<T>[]) =>
|
const hideMoneyColumns = <T>(cols: TableColumn<T>[]) =>
|
||||||
this.showFinancial ? cols : cols.filter((c) => c.type !== 'money');
|
this.showFinancial ? cols : cols.filter((c) => c.type !== 'money');
|
||||||
|
|
@ -1059,12 +1045,12 @@ export class Resumo implements OnInit, AfterViewInit, OnDestroy {
|
||||||
});
|
});
|
||||||
this.macrophonyGroups = groups;
|
this.macrophonyGroups = groups;
|
||||||
|
|
||||||
const search = normalizeAccentInsensitive(this.macrophonySearch);
|
const search = this.normalizeText(this.macrophonySearch);
|
||||||
this.macrophonyFiltered = !search
|
this.macrophonyFiltered = !search
|
||||||
? groups
|
? groups
|
||||||
: groups.filter((group) =>
|
: groups.filter((group) =>
|
||||||
normalizeAccentInsensitive(group.plano).includes(search) ||
|
this.normalizeText(group.plano).includes(search) ||
|
||||||
normalizeAccentInsensitive(group.gbLabel).includes(search)
|
this.normalizeText(group.gbLabel).includes(search)
|
||||||
);
|
);
|
||||||
|
|
||||||
const totalPages = Math.max(1, Math.ceil(this.macrophonyFiltered.length / this.macrophonyPageSize));
|
const totalPages = Math.max(1, Math.ceil(this.macrophonyFiltered.length / this.macrophonyPageSize));
|
||||||
|
|
@ -1100,12 +1086,12 @@ export class Resumo implements OnInit, AfterViewInit, OnDestroy {
|
||||||
if (group.groupSort) groups.sort(group.groupSort);
|
if (group.groupSort) groups.sort(group.groupSort);
|
||||||
group.groups = groups;
|
group.groups = groups;
|
||||||
|
|
||||||
const search = normalizeAccentInsensitive(group.search);
|
const search = this.normalizeText(group.search);
|
||||||
group.filtered = !search
|
group.filtered = !search
|
||||||
? groups
|
? groups
|
||||||
: groups.filter((g) =>
|
: groups.filter((g) =>
|
||||||
normalizeAccentInsensitive(g.title).includes(search) ||
|
this.normalizeText(g.title).includes(search) ||
|
||||||
normalizeAccentInsensitive(g.subtitle).includes(search)
|
this.normalizeText(g.subtitle).includes(search)
|
||||||
);
|
);
|
||||||
|
|
||||||
const totalPages = Math.max(1, Math.ceil(group.filtered.length / group.pageSize));
|
const totalPages = Math.max(1, Math.ceil(group.filtered.length / group.pageSize));
|
||||||
|
|
@ -1120,6 +1106,15 @@ export class Resumo implements OnInit, AfterViewInit, OnDestroy {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private normalizeText(value: any): string {
|
||||||
|
return (value ?? '')
|
||||||
|
.toString()
|
||||||
|
.trim()
|
||||||
|
.toUpperCase()
|
||||||
|
.normalize('NFD')
|
||||||
|
.replace(/[\u0300-\u036f]/g, '');
|
||||||
|
}
|
||||||
|
|
||||||
private sumGroup<T>(rows: T[], getter: (row: T) => any): number {
|
private sumGroup<T>(rows: T[], getter: (row: T) => any): number {
|
||||||
return rows.reduce((acc, row) => acc + (this.toNumber(getter(row)) ?? 0), 0);
|
return rows.reduce((acc, row) => acc + (this.toNumber(getter(row)) ?? 0), 0);
|
||||||
}
|
}
|
||||||
|
|
@ -1138,7 +1133,7 @@ export class Resumo implements OnInit, AfterViewInit, OnDestroy {
|
||||||
|
|
||||||
rows.forEach((row) => {
|
rows.forEach((row) => {
|
||||||
const planoContrato = (row.planoContrato ?? '-').toString().trim() || '-';
|
const planoContrato = (row.planoContrato ?? '-').toString().trim() || '-';
|
||||||
const key = normalizeAccentInsensitive(planoContrato);
|
const key = this.normalizeText(planoContrato);
|
||||||
const gb =
|
const gb =
|
||||||
this.extractGbFromPlanName(planoContrato) ??
|
this.extractGbFromPlanName(planoContrato) ??
|
||||||
this.toNumber(row.gb ?? row.franquiaGb);
|
this.toNumber(row.gb ?? row.franquiaGb);
|
||||||
|
|
@ -1219,59 +1214,78 @@ export class Resumo implements OnInit, AfterViewInit, OnDestroy {
|
||||||
return Number.isNaN(parsed) ? null : parsed;
|
return Number.isNaN(parsed) ? null : parsed;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async exportTableAsXlsx<T>(table: TableState<T>, fileKey: string): Promise<void> {
|
private exportCsv<T>(table: TableState<T>, filename: string) {
|
||||||
if (!isPlatformBrowser(this.platformId)) return;
|
if (!isPlatformBrowser(this.platformId)) return;
|
||||||
if (this.exportingKeys.has(fileKey)) return;
|
|
||||||
|
|
||||||
const rows = table.data ?? [];
|
const rows = table.data ?? [];
|
||||||
if (!rows.length) {
|
const columns = table.columns ?? [];
|
||||||
this.showToast('Nenhum registro encontrado para exportar.', 'danger');
|
const generatedAt = new Date().toLocaleString('pt-BR');
|
||||||
return;
|
const escapeHtml = (value: string) =>
|
||||||
}
|
value
|
||||||
|
.replace(/&/g, '&')
|
||||||
|
.replace(/</g, '<')
|
||||||
|
.replace(/>/g, '>')
|
||||||
|
.replace(/"/g, '"')
|
||||||
|
.replace(/'/g, ''');
|
||||||
|
|
||||||
this.exportingKeys.add(fileKey);
|
const headerHtml = columns
|
||||||
try {
|
.map((column) => `<th class="${column.align === 'right' ? 'text-right' : column.align === 'center' ? 'text-center' : ''}">${escapeHtml(column.label)}</th>`)
|
||||||
const timestamp = this.tableExportService.buildTimestamp();
|
.join('');
|
||||||
await this.tableExportService.exportAsXlsx<T>({
|
|
||||||
fileName: `${fileKey}_${timestamp}`,
|
|
||||||
sheetName: table.label || 'Resumo',
|
|
||||||
rows,
|
|
||||||
columns: (table.columns ?? []).map((column) => ({
|
|
||||||
header: column.label,
|
|
||||||
type: this.mapColumnType(column.type),
|
|
||||||
value: (row: T) => this.getExportColumnValue(column, row),
|
|
||||||
})),
|
|
||||||
});
|
|
||||||
|
|
||||||
this.showToast(`Planilha exportada com ${rows.length} registro(s).`, 'success');
|
const bodyHtml = rows
|
||||||
} catch {
|
.map((row, index) => {
|
||||||
this.showToast('Erro ao exportar planilha.', 'danger');
|
const cells = columns
|
||||||
} finally {
|
.map((column) => {
|
||||||
this.exportingKeys.delete(fileKey);
|
const value = this.formatCell(column, row);
|
||||||
}
|
const toneClass = column.tone ? this.getToneClass(column.value(row)) : '';
|
||||||
}
|
const alignClass = column.align === 'right' ? 'text-right' : column.align === 'center' ? 'text-center' : '';
|
||||||
|
const classes = [alignClass, toneClass].filter(Boolean).join(' ');
|
||||||
|
return `<td class="${classes}">${escapeHtml(String(value))}</td>`;
|
||||||
|
})
|
||||||
|
.join('');
|
||||||
|
return `<tr class="${index % 2 === 0 ? 'even' : 'odd'}">${cells}</tr>`;
|
||||||
|
})
|
||||||
|
.join('');
|
||||||
|
|
||||||
private getExportColumnValue<T>(column: TableColumn<T>, row: T): unknown {
|
const html = `<!DOCTYPE html>
|
||||||
const rawValue = column.value(row);
|
<html lang="pt-BR">
|
||||||
if (column.type === 'money' || column.type === 'number' || column.type === 'gb') {
|
<head>
|
||||||
const numeric = this.toNumber(rawValue);
|
<meta charset="UTF-8" />
|
||||||
if (numeric !== null) return numeric;
|
<style>
|
||||||
}
|
body { font-family: Segoe UI, Arial, sans-serif; margin: 20px; color: #0f172a; }
|
||||||
return this.formatCell(column, row);
|
.sheet-title { font-size: 18px; font-weight: 700; margin-bottom: 4px; }
|
||||||
}
|
.sheet-subtitle { font-size: 12px; color: #64748b; margin-bottom: 14px; }
|
||||||
|
table { border-collapse: collapse; width: 100%; table-layout: auto; }
|
||||||
|
th, td { border: 1px solid #dbe2ef; padding: 8px 10px; font-size: 12px; }
|
||||||
|
th { background: #e8eefc; color: #1e3a8a; font-weight: 700; text-transform: uppercase; letter-spacing: 0.3px; }
|
||||||
|
tr.even td { background: #ffffff; }
|
||||||
|
tr.odd td { background: #f8fafc; }
|
||||||
|
.text-right { text-align: right; }
|
||||||
|
.text-center { text-align: center; }
|
||||||
|
.text-success { color: #047857; font-weight: 700; }
|
||||||
|
.text-danger { color: #b91c1c; font-weight: 700; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="sheet-title">${escapeHtml(table.label || 'Resumo')}</div>
|
||||||
|
<div class="sheet-subtitle">Exportado em ${escapeHtml(generatedAt)} | Total de linhas: ${rows.length}</div>
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>${headerHtml}</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
${bodyHtml}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</body>
|
||||||
|
</html>`;
|
||||||
|
|
||||||
private mapColumnType(type: TableColumn<any>['type']): ExportCellType {
|
const blob = new Blob([`\uFEFF${html}`], { type: 'application/vnd.ms-excel;charset=utf-8;' });
|
||||||
if (type === 'money') return 'currency';
|
const url = URL.createObjectURL(blob);
|
||||||
if (type === 'number' || type === 'gb') return 'number';
|
const a = document.createElement('a');
|
||||||
return 'text';
|
a.href = url;
|
||||||
}
|
a.download = `${filename}.xls`;
|
||||||
|
a.click();
|
||||||
private showToast(message: string, type: 'success' | 'danger'): void {
|
URL.revokeObjectURL(url);
|
||||||
this.toastMessage = message;
|
|
||||||
this.toastType = type;
|
|
||||||
this.toastOpen = true;
|
|
||||||
if (this.toastTimer) clearTimeout(this.toastTimer);
|
|
||||||
this.toastTimer = setTimeout(() => (this.toastOpen = false), 3000);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private getReservaPorDddChartData(): Array<{ label: string; totalLinhas: number }> {
|
private getReservaPorDddChartData(): Array<{ label: string; totalLinhas: number }> {
|
||||||
|
|
@ -1296,7 +1310,7 @@ export class Resumo implements OnInit, AfterViewInit, OnDestroy {
|
||||||
return Array.from(map.entries()).map(([label, totalLinhas]) => ({ label, totalLinhas }));
|
return Array.from(map.entries()).map(([label, totalLinhas]) => ({ label, totalLinhas }));
|
||||||
}
|
}
|
||||||
|
|
||||||
exportMacrophonyCsv() { void this.exportTableAsXlsx(this.tableMacrophony, 'macrophony-planos'); }
|
exportMacrophonyCsv() { this.exportCsv(this.tableMacrophony, 'macrophony-planos'); }
|
||||||
findLineTotal(k: string[]): LineTotal | null {
|
findLineTotal(k: string[]): LineTotal | null {
|
||||||
const keys = k.map((item) => item.toUpperCase());
|
const keys = k.map((item) => item.toUpperCase());
|
||||||
const list = this.getEffectiveLineTotais();
|
const list = this.getEffectiveLineTotais();
|
||||||
|
|
@ -1321,13 +1335,19 @@ export class Resumo implements OnInit, AfterViewInit, OnDestroy {
|
||||||
return Array.isArray(this.resumo?.lineTotais) ? (this.resumo?.lineTotais ?? []) : [];
|
return Array.isArray(this.resumo?.lineTotais) ? (this.resumo?.lineTotais ?? []) : [];
|
||||||
}
|
}
|
||||||
|
|
||||||
get macrophonyPageStart() { return computePageStart(this.macrophonyFilteredGroups.length, this.macrophonyPage, this.macrophonyPageSize); }
|
get macrophonyPageStart() { return (this.macrophonyPage - 1) * this.macrophonyPageSize + 1; }
|
||||||
get macrophonyPageEnd() { return computePageEnd(this.macrophonyFilteredGroups.length, this.macrophonyPage, this.macrophonyPageSize); }
|
get macrophonyPageEnd() { return Math.min(this.macrophonyPage * this.macrophonyPageSize, this.macrophonyFilteredGroups.length); }
|
||||||
get macrophonyFilteredGroups() { return this.macrophonyFiltered; }
|
get macrophonyFilteredGroups() { return this.macrophonyFiltered; }
|
||||||
get macrophonyPageNumbers() {
|
get macrophonyPageNumbers() {
|
||||||
return buildPageNumbers(this.macrophonyPage, this.macrophonyTotalPages);
|
const total = this.macrophonyTotalPages;
|
||||||
|
if (total <= 1) return [1];
|
||||||
|
const current = Math.min(Math.max(this.macrophonyPage, 1), total);
|
||||||
|
const start = Math.max(1, current - 2);
|
||||||
|
const end = Math.min(total, start + 4);
|
||||||
|
const adjustedStart = Math.max(1, end - 4);
|
||||||
|
return Array.from({ length: end - adjustedStart + 1 }, (_, i) => adjustedStart + i);
|
||||||
}
|
}
|
||||||
get macrophonyTotalPages() { return computeTotalPages(this.macrophonyFiltered.length, this.macrophonyPageSize); }
|
get macrophonyTotalPages() { return Math.max(1, Math.ceil(this.macrophonyFiltered.length / this.macrophonyPageSize)); }
|
||||||
get planosTotals() { return this.resumo?.macrophonyTotals; }
|
get planosTotals() { return this.resumo?.macrophonyTotals; }
|
||||||
get contratosTotals() { return this.resumo?.planoContratoTotal; }
|
get contratosTotals() { return this.resumo?.planoContratoTotal; }
|
||||||
get clientesTotals() { return this.resumo?.vivoLineTotals; }
|
get clientesTotals() { return this.resumo?.vivoLineTotals; }
|
||||||
|
|
|
||||||
|
|
@ -1,138 +0,0 @@
|
||||||
<section class="solicitacoes-page">
|
|
||||||
<div class="container-geral-responsive">
|
|
||||||
<div class="geral-card">
|
|
||||||
<div class="geral-header">
|
|
||||||
<div class="header-row-top">
|
|
||||||
<div class="title-badge">
|
|
||||||
<i class="bi bi-envelope-paper"></i> Administração
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="header-title">
|
|
||||||
<h5 class="title mb-0">Solicitações</h5>
|
|
||||||
<small class="subtitle">Pedidos de alteração de franquia e bloqueio enviados pelos usuários.</small>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="header-actions d-flex gap-2 justify-content-end">
|
|
||||||
<button type="button" class="btn btn-brand btn-sm" (click)="refresh()" [disabled]="loading">
|
|
||||||
<i class="bi bi-arrow-clockwise me-1"></i> Atualizar
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="controls mt-3">
|
|
||||||
<div class="input-group input-group-sm search-group">
|
|
||||||
<span class="input-group-text">
|
|
||||||
<i class="bi" [class.bi-search]="!loading" [class.bi-hourglass-split]="loading"></i>
|
|
||||||
</span>
|
|
||||||
<input
|
|
||||||
class="form-control"
|
|
||||||
placeholder="Pesquisar por linha, usuário da linha ou descrição..."
|
|
||||||
[(ngModel)]="search"
|
|
||||||
(ngModelChange)="onSearchChange()" />
|
|
||||||
<button class="btn btn-outline-secondary btn-clear" type="button" (click)="clearSearch()" *ngIf="search">
|
|
||||||
<i class="bi bi-x-lg"></i>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="page-size d-flex align-items-center gap-2">
|
|
||||||
<span class="text-muted small fw-bold text-uppercase" style="letter-spacing: 0.5px; font-size: 0.75rem;">
|
|
||||||
Itens por pág:
|
|
||||||
</span>
|
|
||||||
<div class="select-wrapper">
|
|
||||||
<app-select
|
|
||||||
class="select-glass"
|
|
||||||
size="sm"
|
|
||||||
[options]="pageSizeOptions"
|
|
||||||
[(ngModel)]="pageSize"
|
|
||||||
(ngModelChange)="onPageSizeChange()"
|
|
||||||
[disabled]="loading"></app-select>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="table-summary" *ngIf="!loading && !errorMsg">
|
|
||||||
Mostrando <strong>{{ pageStart }}</strong>-<strong>{{ pageEnd }}</strong> de <strong>{{ total }}</strong>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="geral-body">
|
|
||||||
<div class="table-wrap">
|
|
||||||
<div class="text-center p-5" *ngIf="loading">
|
|
||||||
<span class="spinner-border text-brand"></span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="alert alert-danger m-4" *ngIf="!loading && errorMsg">
|
|
||||||
{{ errorMsg }}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="empty-group" *ngIf="!loading && !errorMsg && items.length === 0">
|
|
||||||
Nenhuma solicitação encontrada.
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<table class="table table-modern align-middle mb-0" *ngIf="!loading && !errorMsg && items.length > 0">
|
|
||||||
<colgroup>
|
|
||||||
<col class="col-date" />
|
|
||||||
<col class="col-cliente" />
|
|
||||||
<col class="col-linha" />
|
|
||||||
<col class="col-usuario" />
|
|
||||||
<col class="col-tipo" />
|
|
||||||
<col class="col-franquia" />
|
|
||||||
<col class="col-franquia" />
|
|
||||||
<col class="col-descricao" />
|
|
||||||
</colgroup>
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>DATA</th>
|
|
||||||
<th>CLIENTE</th>
|
|
||||||
<th>LINHA</th>
|
|
||||||
<th>USUARIO LINHA</th>
|
|
||||||
<th>TIPO</th>
|
|
||||||
<th>FRANQUIA ANTES</th>
|
|
||||||
<th>FRANQUIA DEPOIS</th>
|
|
||||||
<th>DESCRICAO</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
<tr *ngFor="let item of items; trackBy: trackBySolicitacao">
|
|
||||||
<td>
|
|
||||||
<div class="date-cell">
|
|
||||||
<span class="date-main">{{ formatDate(item.createdAt) }}</span>
|
|
||||||
<span class="date-sub">{{ formatTime(item.createdAt) }}</span>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
<td class="cell-ellipsis" [title]="item.tenantNome || '-'">{{ item.tenantNome || '-' }}</td>
|
|
||||||
<td class="mono-cell">{{ item.linha || '-' }}</td>
|
|
||||||
<td class="cell-ellipsis" [title]="item.usuarioLinha || '-'">{{ item.usuarioLinha || '-' }}</td>
|
|
||||||
<td>
|
|
||||||
<span [class]="tipoBadgeClass(item.tipoSolicitacao)">{{ tipoLabel(item.tipoSolicitacao) }}</span>
|
|
||||||
</td>
|
|
||||||
<td class="franquia-cell">{{ formatFranquia(item.franquiaLineAtual) }}</td>
|
|
||||||
<td class="franquia-cell">{{ formatFranquia(item.franquiaLineNova) }}</td>
|
|
||||||
<td class="message-cell" [title]="descricao(item)">
|
|
||||||
<span class="message-text">{{ descricao(item) }}</span>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="geral-footer">
|
|
||||||
<div class="small text-muted fw-bold">Mostrando {{ pageStart }}–{{ pageEnd }} de {{ total }} registros</div>
|
|
||||||
<nav>
|
|
||||||
<ul class="pagination pagination-sm mb-0 pagination-modern">
|
|
||||||
<li class="page-item" [class.disabled]="page === 1 || loading">
|
|
||||||
<button class="page-link" (click)="goToPage(page - 1)">Anterior</button>
|
|
||||||
</li>
|
|
||||||
<li class="page-item" *ngFor="let p of pageNumbers" [class.active]="p === page">
|
|
||||||
<button class="page-link" (click)="goToPage(p)">{{ p }}</button>
|
|
||||||
</li>
|
|
||||||
<li class="page-item" [class.disabled]="page === totalPages || loading">
|
|
||||||
<button class="page-link" (click)="goToPage(page + 1)">Próxima</button>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</nav>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
@ -1,368 +0,0 @@
|
||||||
:host {
|
|
||||||
--brand: #e33dcf;
|
|
||||||
--text: #111214;
|
|
||||||
--card-bg: rgba(255, 255, 255, 0.88);
|
|
||||||
--card-border: 1px solid rgba(227, 61, 207, 0.14);
|
|
||||||
--stroke: rgba(16, 24, 40, 0.08);
|
|
||||||
--soft: rgba(17, 18, 20, 0.64);
|
|
||||||
|
|
||||||
display: block;
|
|
||||||
color: var(--text);
|
|
||||||
}
|
|
||||||
|
|
||||||
.solicitacoes-page {
|
|
||||||
min-height: 100vh;
|
|
||||||
padding: 0 0 18px;
|
|
||||||
background:
|
|
||||||
radial-gradient(920px 420px at 15% 8%, rgba(227, 61, 207, 0.1), transparent 60%),
|
|
||||||
radial-gradient(860px 420px at 85% 20%, rgba(3, 15, 170, 0.07), transparent 60%),
|
|
||||||
linear-gradient(180deg, #ffffff 0%, #f5f6fb 70%);
|
|
||||||
}
|
|
||||||
|
|
||||||
.container-geral-responsive {
|
|
||||||
width: calc(100vw - 2px);
|
|
||||||
max-width: none;
|
|
||||||
margin: 16px auto 24px;
|
|
||||||
position: relative;
|
|
||||||
z-index: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.geral-card {
|
|
||||||
border-radius: 22px;
|
|
||||||
overflow: hidden;
|
|
||||||
background: var(--card-bg);
|
|
||||||
border: var(--card-border);
|
|
||||||
backdrop-filter: blur(8px);
|
|
||||||
box-shadow: 0 18px 40px rgba(17, 18, 20, 0.1);
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
min-height: 64vh;
|
|
||||||
}
|
|
||||||
|
|
||||||
.geral-header {
|
|
||||||
padding: 16px 22px 14px;
|
|
||||||
border-bottom: 1px solid var(--stroke);
|
|
||||||
background: linear-gradient(180deg, rgba(227, 61, 207, 0.05), rgba(255, 255, 255, 0.16));
|
|
||||||
}
|
|
||||||
|
|
||||||
.header-row-top {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: 1fr auto 1fr;
|
|
||||||
gap: 12px;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.title-badge {
|
|
||||||
justify-self: start;
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 8px;
|
|
||||||
padding: 7px 12px;
|
|
||||||
border-radius: 999px;
|
|
||||||
border: 1px solid rgba(227, 61, 207, 0.2);
|
|
||||||
background: rgba(255, 255, 255, 0.8);
|
|
||||||
font-weight: 800;
|
|
||||||
font-size: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.header-title {
|
|
||||||
justify-self: center;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.title {
|
|
||||||
font-size: 24px;
|
|
||||||
font-weight: 900;
|
|
||||||
letter-spacing: -0.01em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.subtitle {
|
|
||||||
color: var(--soft);
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
|
|
||||||
.header-actions {
|
|
||||||
justify-self: end;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-brand {
|
|
||||||
background: var(--brand);
|
|
||||||
border-color: var(--brand);
|
|
||||||
color: #fff;
|
|
||||||
font-weight: 800;
|
|
||||||
}
|
|
||||||
|
|
||||||
.controls {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: minmax(320px, 1.3fr) auto auto;
|
|
||||||
align-items: center;
|
|
||||||
gap: 14px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.search-group {
|
|
||||||
max-width: 700px;
|
|
||||||
width: 100%;
|
|
||||||
border-radius: 13px;
|
|
||||||
border: 1px solid rgba(16, 24, 40, 0.16);
|
|
||||||
overflow: hidden;
|
|
||||||
box-shadow: 0 4px 10px rgba(16, 24, 40, 0.05);
|
|
||||||
background: #fff;
|
|
||||||
|
|
||||||
.input-group-text,
|
|
||||||
.form-control,
|
|
||||||
.btn-clear {
|
|
||||||
border: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.input-group-text {
|
|
||||||
color: rgba(17, 18, 20, 0.55);
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-control {
|
|
||||||
font-size: 13px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.geral-body {
|
|
||||||
flex: 1;
|
|
||||||
padding: 0 4px 10px;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.table-wrap {
|
|
||||||
width: 100%;
|
|
||||||
min-width: 0;
|
|
||||||
max-width: 100%;
|
|
||||||
min-height: clamp(260px, 44vh, 500px);
|
|
||||||
max-height: 60vh;
|
|
||||||
border: 1px solid rgba(16, 24, 40, 0.1);
|
|
||||||
border-radius: 14px;
|
|
||||||
overflow-x: hidden;
|
|
||||||
overflow-y: auto;
|
|
||||||
background:
|
|
||||||
linear-gradient(180deg, rgba(255, 255, 255, 0.98) 0%, rgba(248, 251, 255, 0.96) 100%);
|
|
||||||
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.75);
|
|
||||||
}
|
|
||||||
|
|
||||||
.table-modern {
|
|
||||||
width: 100%;
|
|
||||||
min-width: 0;
|
|
||||||
table-layout: fixed;
|
|
||||||
}
|
|
||||||
|
|
||||||
.table-modern col.col-date { width: 9%; }
|
|
||||||
.table-modern col.col-cliente { width: 14%; }
|
|
||||||
.table-modern col.col-linha { width: 10%; }
|
|
||||||
.table-modern col.col-usuario { width: 15%; }
|
|
||||||
.table-modern col.col-tipo { width: 14%; }
|
|
||||||
.table-modern col.col-franquia { width: 12%; }
|
|
||||||
.table-modern col.col-descricao { width: 14%; }
|
|
||||||
|
|
||||||
.table-modern thead th {
|
|
||||||
position: sticky;
|
|
||||||
top: 0;
|
|
||||||
z-index: 2;
|
|
||||||
background: #f6f8fc;
|
|
||||||
font-size: 11px;
|
|
||||||
font-weight: 800;
|
|
||||||
text-transform: uppercase;
|
|
||||||
letter-spacing: 0.04em;
|
|
||||||
color: rgba(17, 18, 20, 0.74);
|
|
||||||
padding: 10px 12px;
|
|
||||||
border-bottom: 1px solid #e4e9f3;
|
|
||||||
white-space: normal;
|
|
||||||
word-break: break-word;
|
|
||||||
line-height: 1.2;
|
|
||||||
}
|
|
||||||
|
|
||||||
.table-modern thead th:nth-child(6),
|
|
||||||
.table-modern thead th:nth-child(7),
|
|
||||||
.table-modern tbody td:nth-child(6),
|
|
||||||
.table-modern tbody td:nth-child(7) {
|
|
||||||
min-width: 0;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.table-modern tbody td {
|
|
||||||
font-size: 13px;
|
|
||||||
padding: 11px 12px;
|
|
||||||
border-bottom: 1px solid #ebeff6;
|
|
||||||
vertical-align: middle;
|
|
||||||
white-space: normal;
|
|
||||||
word-break: break-word;
|
|
||||||
color: #1a1c20;
|
|
||||||
}
|
|
||||||
|
|
||||||
.table-modern tbody tr:nth-child(even) {
|
|
||||||
background: rgba(243, 247, 253, 0.56);
|
|
||||||
}
|
|
||||||
|
|
||||||
.table-modern tbody tr:hover {
|
|
||||||
background: #eef5ff;
|
|
||||||
}
|
|
||||||
|
|
||||||
.date-cell {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
line-height: 1.15;
|
|
||||||
gap: 3px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.date-main {
|
|
||||||
font-weight: 700;
|
|
||||||
color: #1a1c20;
|
|
||||||
}
|
|
||||||
|
|
||||||
.date-sub {
|
|
||||||
font-size: 11px;
|
|
||||||
color: rgba(17, 18, 20, 0.55);
|
|
||||||
}
|
|
||||||
|
|
||||||
.mono-cell {
|
|
||||||
font-family: "JetBrains Mono", "Consolas", "Monaco", monospace;
|
|
||||||
font-variant-numeric: tabular-nums;
|
|
||||||
font-size: 12px;
|
|
||||||
font-weight: 700;
|
|
||||||
white-space: normal;
|
|
||||||
word-break: break-all;
|
|
||||||
}
|
|
||||||
|
|
||||||
.cell-ellipsis {
|
|
||||||
overflow: visible;
|
|
||||||
text-overflow: unset;
|
|
||||||
white-space: normal;
|
|
||||||
}
|
|
||||||
|
|
||||||
.type-badge {
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
border-radius: 999px;
|
|
||||||
padding: 5px 10px;
|
|
||||||
font-size: 11px;
|
|
||||||
font-weight: 800;
|
|
||||||
line-height: 1;
|
|
||||||
border: 1px solid transparent;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.type-badge--franquia {
|
|
||||||
color: #1e4e8f;
|
|
||||||
background: rgba(44, 121, 232, 0.14);
|
|
||||||
border-color: rgba(44, 121, 232, 0.24);
|
|
||||||
}
|
|
||||||
|
|
||||||
.type-badge--bloqueio {
|
|
||||||
color: #8f2f2f;
|
|
||||||
background: rgba(221, 74, 74, 0.14);
|
|
||||||
border-color: rgba(221, 74, 74, 0.25);
|
|
||||||
}
|
|
||||||
|
|
||||||
.type-badge--default {
|
|
||||||
color: #5b6173;
|
|
||||||
background: rgba(118, 127, 154, 0.14);
|
|
||||||
border-color: rgba(118, 127, 154, 0.24);
|
|
||||||
}
|
|
||||||
|
|
||||||
.franquia-cell {
|
|
||||||
text-align: center;
|
|
||||||
font-weight: 700;
|
|
||||||
font-variant-numeric: tabular-nums;
|
|
||||||
}
|
|
||||||
|
|
||||||
.message-cell {
|
|
||||||
white-space: normal;
|
|
||||||
}
|
|
||||||
|
|
||||||
.message-text {
|
|
||||||
display: -webkit-box;
|
|
||||||
-webkit-box-orient: vertical;
|
|
||||||
-webkit-line-clamp: 2;
|
|
||||||
overflow: hidden;
|
|
||||||
line-height: 1.32;
|
|
||||||
color: rgba(17, 18, 20, 0.8);
|
|
||||||
word-break: break-word;
|
|
||||||
}
|
|
||||||
|
|
||||||
.table-summary {
|
|
||||||
justify-self: end;
|
|
||||||
font-size: 12px;
|
|
||||||
color: rgba(17, 18, 20, 0.64);
|
|
||||||
font-weight: 600;
|
|
||||||
|
|
||||||
strong {
|
|
||||||
color: #111214;
|
|
||||||
font-weight: 800;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.empty-group {
|
|
||||||
padding: 36px 18px;
|
|
||||||
text-align: center;
|
|
||||||
color: rgba(17, 18, 20, 0.64);
|
|
||||||
}
|
|
||||||
|
|
||||||
.geral-footer {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.table-wrap::-webkit-scrollbar {
|
|
||||||
height: 10px;
|
|
||||||
width: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.table-wrap::-webkit-scrollbar-track {
|
|
||||||
background: rgba(17, 18, 20, 0.06);
|
|
||||||
}
|
|
||||||
|
|
||||||
.table-wrap::-webkit-scrollbar-thumb {
|
|
||||||
background: rgba(103, 114, 143, 0.45);
|
|
||||||
border-radius: 999px;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 1200px) {
|
|
||||||
.controls {
|
|
||||||
grid-template-columns: 1fr auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.table-summary {
|
|
||||||
grid-column: 1 / -1;
|
|
||||||
justify-self: start;
|
|
||||||
}
|
|
||||||
|
|
||||||
.table-modern col.col-date { width: 10%; }
|
|
||||||
.table-modern col.col-cliente { width: 13%; }
|
|
||||||
.table-modern col.col-linha { width: 9%; }
|
|
||||||
.table-modern col.col-usuario { width: 14%; }
|
|
||||||
.table-modern col.col-tipo { width: 14%; }
|
|
||||||
.table-modern col.col-franquia { width: 13%; }
|
|
||||||
.table-modern col.col-descricao { width: 14%; }
|
|
||||||
|
|
||||||
.table-modern thead th,
|
|
||||||
.table-modern tbody td {
|
|
||||||
padding: 9px 8px;
|
|
||||||
font-size: 11px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 992px) {
|
|
||||||
.header-row-top {
|
|
||||||
grid-template-columns: 1fr;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.title-badge,
|
|
||||||
.header-title,
|
|
||||||
.header-actions {
|
|
||||||
justify-self: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.controls {
|
|
||||||
grid-template-columns: 1fr;
|
|
||||||
align-items: start;
|
|
||||||
}
|
|
||||||
|
|
||||||
.message-text {
|
|
||||||
-webkit-line-clamp: 3;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,186 +0,0 @@
|
||||||
import { Component, OnDestroy, OnInit } from '@angular/core';
|
|
||||||
import { CommonModule } from '@angular/common';
|
|
||||||
import { FormsModule } from '@angular/forms';
|
|
||||||
import { CustomSelectComponent } from '../../components/custom-select/custom-select';
|
|
||||||
import { SolicitacaoLinhaDto, SolicitacoesLinhasService } from '../../services/solicitacoes-linhas.service';
|
|
||||||
import {
|
|
||||||
buildPageNumbers,
|
|
||||||
clampPage,
|
|
||||||
computePageEnd,
|
|
||||||
computePageStart,
|
|
||||||
computeTotalPages
|
|
||||||
} from '../../utils/pagination.util';
|
|
||||||
|
|
||||||
@Component({
|
|
||||||
selector: 'app-solicitacoes-linhas',
|
|
||||||
standalone: true,
|
|
||||||
imports: [CommonModule, FormsModule, CustomSelectComponent],
|
|
||||||
templateUrl: './solicitacoes-linhas.html',
|
|
||||||
styleUrls: ['./solicitacoes-linhas.scss'],
|
|
||||||
})
|
|
||||||
export class SolicitacoesLinhas implements OnInit, OnDestroy {
|
|
||||||
items: SolicitacaoLinhaDto[] = [];
|
|
||||||
loading = false;
|
|
||||||
errorMsg = '';
|
|
||||||
|
|
||||||
page = 1;
|
|
||||||
pageSize = 20;
|
|
||||||
pageSizeOptions = [10, 20, 50, 100];
|
|
||||||
total = 0;
|
|
||||||
|
|
||||||
search = '';
|
|
||||||
private searchTimer: ReturnType<typeof setTimeout> | null = null;
|
|
||||||
|
|
||||||
constructor(private readonly solicitacoesService: SolicitacoesLinhasService) {}
|
|
||||||
|
|
||||||
ngOnInit(): void {
|
|
||||||
this.fetch(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
ngOnDestroy(): void {
|
|
||||||
if (this.searchTimer) {
|
|
||||||
clearTimeout(this.searchTimer);
|
|
||||||
this.searchTimer = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
refresh(): void {
|
|
||||||
this.fetch();
|
|
||||||
}
|
|
||||||
|
|
||||||
onSearchChange(): void {
|
|
||||||
if (this.searchTimer) {
|
|
||||||
clearTimeout(this.searchTimer);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.searchTimer = setTimeout(() => {
|
|
||||||
this.page = 1;
|
|
||||||
this.fetch();
|
|
||||||
}, 300);
|
|
||||||
}
|
|
||||||
|
|
||||||
clearSearch(): void {
|
|
||||||
this.search = '';
|
|
||||||
this.page = 1;
|
|
||||||
this.fetch();
|
|
||||||
}
|
|
||||||
|
|
||||||
onPageSizeChange(): void {
|
|
||||||
this.page = 1;
|
|
||||||
this.fetch();
|
|
||||||
}
|
|
||||||
|
|
||||||
goToPage(pageNumber: number): void {
|
|
||||||
this.page = clampPage(pageNumber, this.totalPages);
|
|
||||||
this.fetch();
|
|
||||||
}
|
|
||||||
|
|
||||||
get totalPages(): number {
|
|
||||||
return computeTotalPages(this.total || 0, this.pageSize);
|
|
||||||
}
|
|
||||||
|
|
||||||
get pageNumbers(): number[] {
|
|
||||||
return buildPageNumbers(this.page, this.totalPages);
|
|
||||||
}
|
|
||||||
|
|
||||||
get pageStart(): number {
|
|
||||||
return computePageStart(this.total || 0, this.page, this.pageSize);
|
|
||||||
}
|
|
||||||
|
|
||||||
get pageEnd(): number {
|
|
||||||
return computePageEnd(this.total || 0, this.page, this.pageSize);
|
|
||||||
}
|
|
||||||
|
|
||||||
private parseDate(value?: string | null): Date | null {
|
|
||||||
if (!value) return null;
|
|
||||||
const d = new Date(value);
|
|
||||||
return Number.isNaN(d.getTime()) ? null : d;
|
|
||||||
}
|
|
||||||
|
|
||||||
formatDateTime(value?: string | null): string {
|
|
||||||
const d = this.parseDate(value);
|
|
||||||
if (!d) return '-';
|
|
||||||
return d.toLocaleString('pt-BR');
|
|
||||||
}
|
|
||||||
|
|
||||||
formatDate(value?: string | null): string {
|
|
||||||
const d = this.parseDate(value);
|
|
||||||
if (!d) return '-';
|
|
||||||
return d.toLocaleDateString('pt-BR');
|
|
||||||
}
|
|
||||||
|
|
||||||
formatTime(value?: string | null): string {
|
|
||||||
const d = this.parseDate(value);
|
|
||||||
if (!d) return '--:--';
|
|
||||||
return d.toLocaleTimeString('pt-BR', { hour: '2-digit', minute: '2-digit' });
|
|
||||||
}
|
|
||||||
|
|
||||||
formatFranquiaValor(value?: number | null): string {
|
|
||||||
if (value === null || value === undefined) return '-';
|
|
||||||
return new Intl.NumberFormat('pt-BR', { maximumFractionDigits: 2 }).format(value);
|
|
||||||
}
|
|
||||||
|
|
||||||
formatFranquia(value?: number | null): string {
|
|
||||||
const formatted = this.formatFranquiaValor(value);
|
|
||||||
return formatted === '-' ? '-' : `${formatted} GB`;
|
|
||||||
}
|
|
||||||
|
|
||||||
tipoLabel(value?: string | null): string {
|
|
||||||
const v = (value ?? '').toString().trim().toUpperCase();
|
|
||||||
if (v === 'ALTERACAO_FRANQUIA') return 'Alteração de franquia';
|
|
||||||
if (v === 'BLOQUEIO') return 'Bloqueio';
|
|
||||||
return v || '-';
|
|
||||||
}
|
|
||||||
|
|
||||||
tipoBadgeClass(value?: string | null): string {
|
|
||||||
const v = (value ?? '').toString().trim().toUpperCase();
|
|
||||||
if (v === 'ALTERACAO_FRANQUIA') return 'type-badge type-badge--franquia';
|
|
||||||
if (v === 'BLOQUEIO') return 'type-badge type-badge--bloqueio';
|
|
||||||
return 'type-badge type-badge--default';
|
|
||||||
}
|
|
||||||
|
|
||||||
trackBySolicitacao(_: number, item: SolicitacaoLinhaDto): string {
|
|
||||||
return item.id;
|
|
||||||
}
|
|
||||||
|
|
||||||
descricao(item: SolicitacaoLinhaDto): string {
|
|
||||||
const tipo = (item.tipoSolicitacao ?? '').toString().trim().toUpperCase();
|
|
||||||
const linha = (item.linha ?? '').toString().trim();
|
|
||||||
|
|
||||||
if (tipo === 'ALTERACAO_FRANQUIA') {
|
|
||||||
return `Mudanca de franquia de ${this.formatFranquiaValor(item.franquiaLineAtual)} para ${this.formatFranquiaValor(item.franquiaLineNova)}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (tipo === 'BLOQUEIO') {
|
|
||||||
return `Bloqueio da linha ${linha || '-'}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (item.mensagem ?? '').toString().trim() || '-';
|
|
||||||
}
|
|
||||||
|
|
||||||
private fetch(goToPage?: number): void {
|
|
||||||
if (goToPage) this.page = goToPage;
|
|
||||||
this.loading = true;
|
|
||||||
this.errorMsg = '';
|
|
||||||
|
|
||||||
this.solicitacoesService
|
|
||||||
.list({
|
|
||||||
page: this.page,
|
|
||||||
pageSize: this.pageSize,
|
|
||||||
search: this.search?.trim() || undefined,
|
|
||||||
})
|
|
||||||
.subscribe({
|
|
||||||
next: (res) => {
|
|
||||||
this.items = res.items || [];
|
|
||||||
this.total = res.total || 0;
|
|
||||||
this.page = res.page || this.page;
|
|
||||||
this.pageSize = res.pageSize || this.pageSize;
|
|
||||||
this.loading = false;
|
|
||||||
},
|
|
||||||
error: () => {
|
|
||||||
this.loading = false;
|
|
||||||
this.errorMsg = 'Não foi possível carregar as solicitações.';
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -31,11 +31,7 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="header-actions d-flex gap-2 justify-content-end" data-animate>
|
<div class="header-actions d-flex gap-2 justify-content-end" data-animate>
|
||||||
<button type="button" class="btn btn-glass btn-sm" (click)="onExport()" [disabled]="loading || exporting">
|
<button type="button" class="btn btn-brand btn-sm" (click)="onCreate()" [disabled]="loading">
|
||||||
<span *ngIf="!exporting"><i class="bi bi-download me-1"></i> Exportar</span>
|
|
||||||
<span *ngIf="exporting"><span class="spinner-border spinner-border-sm me-2"></span> Exportando...</span>
|
|
||||||
</button>
|
|
||||||
<button *ngIf="canManageRecords" type="button" class="btn btn-brand btn-sm" (click)="onCreate()" [disabled]="loading">
|
|
||||||
<i class="bi bi-plus-circle me-1"></i> Nova Troca
|
<i class="bi bi-plus-circle me-1"></i> Nova Troca
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -90,6 +86,7 @@
|
||||||
<span class="text-muted small fw-bold text-uppercase" style="letter-spacing: 0.5px; font-size: 0.75rem;">Itens por pág:</span>
|
<span class="text-muted small fw-bold text-uppercase" style="letter-spacing: 0.5px; font-size: 0.75rem;">Itens por pág:</span>
|
||||||
<div class="select-wrapper">
|
<div class="select-wrapper">
|
||||||
<app-select class="select-glass" size="sm" [options]="pageSizeOptions" [(ngModel)]="pageSize" (ngModelChange)="onPageSizeChange()" [disabled]="loading"></app-select>
|
<app-select class="select-glass" size="sm" [options]="pageSizeOptions" [(ngModel)]="pageSize" (ngModelChange)="onPageSizeChange()" [disabled]="loading"></app-select>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -156,7 +153,7 @@
|
||||||
<td class="td-clip" style="max-width: 360px;" [title]="r.observacao">{{ r.observacao || '-' }}</td>
|
<td class="td-clip" style="max-width: 360px;" [title]="r.observacao">{{ r.observacao || '-' }}</td>
|
||||||
<td>
|
<td>
|
||||||
<div class="action-group justify-content-center">
|
<div class="action-group justify-content-center">
|
||||||
<button *ngIf="canManageRecords" class="btn-icon primary" (click)="onEditar(r)" title="Editar Registro">
|
<button class="btn-icon primary" (click)="onEditar(r)" title="Editar Registro">
|
||||||
<i class="bi bi-pencil-square"></i>
|
<i class="bi bi-pencil-square"></i>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -197,4 +194,184 @@
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<app-troca-numero-modals [vm]="$any(vm)"></app-troca-numero-modals>
|
<div class="modal-backdrop-custom" *ngIf="editOpen || createOpen" (click)="closeEdit(); closeCreate()"></div>
|
||||||
|
|
||||||
|
<!-- EDIT MODAL -->
|
||||||
|
<div class="modal-custom" *ngIf="editOpen">
|
||||||
|
<div class="modal-card modal-lg" (click)="$event.stopPropagation()">
|
||||||
|
<div class="modal-header">
|
||||||
|
<div class="modal-title">
|
||||||
|
<span class="icon-bg primary-soft"><i class="bi bi-pencil-square"></i></span>
|
||||||
|
Editar Troca de Número
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="d-flex align-items-center gap-2">
|
||||||
|
<button class="btn btn-glass btn-sm" (click)="closeEdit()" [disabled]="editSaving">
|
||||||
|
<i class="bi bi-x-lg me-1"></i> Cancelar
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button class="btn btn-brand btn-sm" (click)="saveEdit()" [disabled]="!editModel || editSaving">
|
||||||
|
<span *ngIf="!editSaving"><i class="bi bi-check2-circle me-1"></i> Salvar</span>
|
||||||
|
<span *ngIf="editSaving"><span class="spinner-border spinner-border-sm me-2"></span> Salvando...</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="modal-body modern-body bg-light-gray">
|
||||||
|
<ng-container *ngIf="editModel; else editLoadingTpl">
|
||||||
|
<div class="details-dashboard">
|
||||||
|
<div class="detail-box w-100">
|
||||||
|
<div class="box-header"><span><i class="bi bi-card-text me-2"></i> Informações</span></div>
|
||||||
|
|
||||||
|
<div class="box-body">
|
||||||
|
<div class="form-grid">
|
||||||
|
<div class="form-field">
|
||||||
|
<label>Item</label>
|
||||||
|
<input class="form-control form-control-sm" [(ngModel)]="editModel.item" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-field">
|
||||||
|
<label>Data Troca</label>
|
||||||
|
<input class="form-control form-control-sm" type="date" [(ngModel)]="editModel.dataTroca" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-field">
|
||||||
|
<label>Linha Antiga</label>
|
||||||
|
<input class="form-control form-control-sm" [(ngModel)]="editModel.linhaAntiga" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-field">
|
||||||
|
<label>Linha Nova</label>
|
||||||
|
<input class="form-control form-control-sm fw-bold text-blue" [(ngModel)]="editModel.linhaNova" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-field span-2">
|
||||||
|
<label>ICCID</label>
|
||||||
|
<input class="form-control form-control-sm font-monospace" [(ngModel)]="editModel.iccid" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-field span-2">
|
||||||
|
<label>Motivo</label>
|
||||||
|
<input class="form-control form-control-sm" [(ngModel)]="editModel.motivo" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-field span-2">
|
||||||
|
<label>Observação</label>
|
||||||
|
<textarea class="form-control form-control-sm" rows="3" [(ngModel)]="editModel.observacao"></textarea>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</ng-container>
|
||||||
|
|
||||||
|
<ng-template #editLoadingTpl>
|
||||||
|
<div class="p-5 text-center text-muted">Preparando edição...</div>
|
||||||
|
</ng-template>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- CREATE MODAL (✅ BEBENDO DO GERAL) -->
|
||||||
|
<div class="modal-custom" *ngIf="createOpen">
|
||||||
|
<div class="modal-card modal-lg" (click)="$event.stopPropagation()">
|
||||||
|
<div class="modal-header">
|
||||||
|
<div class="modal-title">
|
||||||
|
<span class="icon-bg brand-soft"><i class="bi bi-plus-lg"></i></span>
|
||||||
|
Nova Troca
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="d-flex align-items-center gap-2">
|
||||||
|
<button class="btn btn-glass btn-sm" (click)="closeCreate()" [disabled]="createSaving">
|
||||||
|
<i class="bi bi-x-lg me-1"></i> Cancelar
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button class="btn btn-brand btn-sm" (click)="saveCreate()" [disabled]="createSaving">
|
||||||
|
<span *ngIf="!createSaving"><i class="bi bi-check2-circle me-1"></i> Criar</span>
|
||||||
|
<span *ngIf="createSaving"><span class="spinner-border spinner-border-sm me-2"></span> Salvando...</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="modal-body modern-body bg-light-gray">
|
||||||
|
<div class="details-dashboard">
|
||||||
|
<div class="detail-box w-100">
|
||||||
|
<div class="box-header"><span><i class="bi bi-pencil me-2"></i> Preencha os dados</span></div>
|
||||||
|
|
||||||
|
<div class="box-body">
|
||||||
|
<div class="form-grid">
|
||||||
|
|
||||||
|
<div class="form-field">
|
||||||
|
<label>Item</label>
|
||||||
|
<input class="form-control form-control-sm" [(ngModel)]="createModel.item" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-field">
|
||||||
|
<label>Data Troca</label>
|
||||||
|
<input class="form-control form-control-sm" type="date" [(ngModel)]="createModel.dataTroca" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ✅ Cliente (GERAL) -->
|
||||||
|
<div class="form-field span-2">
|
||||||
|
<label>Cliente (GERAL)</label>
|
||||||
|
<app-select class="form-control" size="sm" [options]="clientsFromGeral" [(ngModel)]="selectedCliente" (ngModelChange)="onClienteChange()" placeholder="Selecione..."></app-select>
|
||||||
|
|
||||||
|
<small class="hint" *ngIf="loadingClients">
|
||||||
|
<span class="spinner-border spinner-border-sm me-2"></span> Carregando clientes...
|
||||||
|
</small>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ✅ Linha do Cliente (GERAL) -->
|
||||||
|
<div class="form-field span-2">
|
||||||
|
<label>Linha do Cliente (GERAL)</label>
|
||||||
|
<app-select class="form-control" size="sm" [options]="linesFromClient" labelKey="label" valueKey="id" [(ngModel)]="selectedLineId" (ngModelChange)="onLineChange()" [disabled]="!selectedCliente || loadingLines" placeholder="Selecione a linha do cliente..."></app-select>
|
||||||
|
|
||||||
|
<small class="hint" *ngIf="loadingLines">
|
||||||
|
<span class="spinner-border spinner-border-sm me-2"></span> Carregando linhas...
|
||||||
|
</small>
|
||||||
|
|
||||||
|
<small class="hint warn" *ngIf="selectedCliente && !loadingLines && linesFromClient.length === 0">
|
||||||
|
Nenhuma linha encontrada para este cliente no GERAL.
|
||||||
|
</small>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ✅ Linha Antiga (auto do GERAL) -->
|
||||||
|
<div class="form-field">
|
||||||
|
<label>Linha Antiga (auto)</label>
|
||||||
|
<input class="form-control form-control-sm" [(ngModel)]="createModel.linhaAntiga" readonly />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Linha Nova -->
|
||||||
|
<div class="form-field">
|
||||||
|
<label>Linha Nova</label>
|
||||||
|
<input class="form-control form-control-sm fw-bold text-blue" [(ngModel)]="createModel.linhaNova" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ✅ ICCID (auto do GERAL) -->
|
||||||
|
<div class="form-field span-2">
|
||||||
|
<label>ICCID (auto)</label>
|
||||||
|
<input class="form-control form-control-sm font-monospace" [(ngModel)]="createModel.iccid" readonly />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-field span-2">
|
||||||
|
<label>Motivo</label>
|
||||||
|
<input class="form-control form-control-sm" [(ngModel)]="createModel.motivo" placeholder="Ex: perda/roubo, troca de colaborador..." />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-field span-2">
|
||||||
|
<label>Observação</label>
|
||||||
|
<textarea class="form-control form-control-sm" rows="3" [(ngModel)]="createModel.observacao"></textarea>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -534,6 +534,9 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
/* MODALS */
|
/* MODALS */
|
||||||
|
.modal-backdrop-custom { position: fixed; inset: 0; background: rgba(0,0,0,0.45); z-index: 9990; backdrop-filter: blur(4px); }
|
||||||
|
.modal-custom { position: fixed; inset: 0; display: flex; align-items: center; justify-content: center; z-index: 9995; padding: 16px; }
|
||||||
|
|
||||||
.modal-card {
|
.modal-card {
|
||||||
background: #ffffff;
|
background: #ffffff;
|
||||||
border: 1px solid rgba(255,255,255,0.8);
|
border: 1px solid rgba(255,255,255,0.8);
|
||||||
|
|
|
||||||
|
|
@ -10,20 +10,8 @@ import {
|
||||||
import { isPlatformBrowser, CommonModule } from '@angular/common';
|
import { isPlatformBrowser, CommonModule } from '@angular/common';
|
||||||
import { FormsModule } from '@angular/forms';
|
import { FormsModule } from '@angular/forms';
|
||||||
import { HttpClient, HttpParams } from '@angular/common/http';
|
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 { CustomSelectComponent } from '../../components/custom-select/custom-select';
|
||||||
import { TableExportService } from '../../services/table-export.service';
|
|
||||||
import { environment } from '../../../environments/environment';
|
import { environment } from '../../../environments/environment';
|
||||||
import { TrocaNumeroModalsComponent } from '../../components/page-modals/troca-numero-modals/troca-numero-modals';
|
|
||||||
import {
|
|
||||||
buildPageNumbers,
|
|
||||||
clampPage,
|
|
||||||
computePageEnd,
|
|
||||||
computePageStart,
|
|
||||||
computeTotalPages
|
|
||||||
} from '../../utils/pagination.util';
|
|
||||||
import { buildApiEndpoint } from '../../utils/api-base.util';
|
|
||||||
|
|
||||||
type TrocaKey = 'item' | 'linhaAntiga' | 'linhaNova' | 'iccid' | 'dataTroca' | 'motivo' | 'observacao';
|
type TrocaKey = 'item' | 'linhaAntiga' | 'linhaNova' | 'iccid' | 'dataTroca' | 'motivo' | 'observacao';
|
||||||
|
|
||||||
|
|
@ -68,30 +56,34 @@ interface LineOptionDto {
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
standalone: true,
|
standalone: true,
|
||||||
imports: [CommonModule, FormsModule, CustomSelectComponent, TrocaNumeroModalsComponent],
|
imports: [CommonModule, FormsModule, CustomSelectComponent],
|
||||||
templateUrl: './troca-numero.html',
|
templateUrl: './troca-numero.html',
|
||||||
styleUrls: ['./troca-numero.scss']
|
styleUrls: ['./troca-numero.scss']
|
||||||
})
|
})
|
||||||
export class TrocaNumero implements AfterViewInit {
|
export class TrocaNumero implements AfterViewInit {
|
||||||
readonly vm = this;
|
|
||||||
toastMessage = '';
|
toastMessage = '';
|
||||||
loading = false;
|
loading = false;
|
||||||
exporting = false;
|
|
||||||
|
|
||||||
@ViewChild('successToast', { static: false }) successToast!: ElementRef;
|
@ViewChild('successToast', { static: false }) successToast!: ElementRef;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
@Inject(PLATFORM_ID) private platformId: object,
|
@Inject(PLATFORM_ID) private platformId: object,
|
||||||
private http: HttpClient,
|
private http: HttpClient,
|
||||||
private cdr: ChangeDetectorRef,
|
private cdr: ChangeDetectorRef
|
||||||
private authService: AuthService,
|
|
||||||
private tableExportService: TableExportService
|
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
private readonly apiBase = buildApiEndpoint(environment.apiUrl, 'trocanumero');
|
private readonly apiBase = (() => {
|
||||||
|
const raw = (environment.apiUrl || '').replace(/\/+$/, '');
|
||||||
|
const apiBase = raw.toLowerCase().endsWith('/api') ? raw : `${raw}/api`;
|
||||||
|
return `${apiBase}/trocanumero`;
|
||||||
|
})();
|
||||||
|
|
||||||
/** ✅ base do GERAL (para buscar clientes/linhas no modal) */
|
/** ✅ base do GERAL (para buscar clientes/linhas no modal) */
|
||||||
private readonly linesApiBase = buildApiEndpoint(environment.apiUrl, 'lines');
|
private readonly linesApiBase = (() => {
|
||||||
|
const raw = (environment.apiUrl || '').replace(/\/+$/, '');
|
||||||
|
const apiBase = raw.toLowerCase().endsWith('/api') ? raw : `${raw}/api`;
|
||||||
|
return `${apiBase}/lines`;
|
||||||
|
})();
|
||||||
|
|
||||||
// ====== DATA ======
|
// ====== DATA ======
|
||||||
groups: GroupItem[] = [];
|
groups: GroupItem[] = [];
|
||||||
|
|
@ -140,20 +132,9 @@ export class TrocaNumero implements AfterViewInit {
|
||||||
loadingClients = false;
|
loadingClients = false;
|
||||||
loadingLines = false;
|
loadingLines = false;
|
||||||
|
|
||||||
isSysAdmin = false;
|
|
||||||
isGestor = false;
|
|
||||||
isFinanceiro = false;
|
|
||||||
|
|
||||||
get canManageRecords(): boolean {
|
|
||||||
return this.isSysAdmin || this.isGestor;
|
|
||||||
}
|
|
||||||
|
|
||||||
async ngAfterViewInit() {
|
async ngAfterViewInit() {
|
||||||
if (!isPlatformBrowser(this.platformId)) return;
|
if (!isPlatformBrowser(this.platformId)) return;
|
||||||
this.initAnimations();
|
this.initAnimations();
|
||||||
this.isSysAdmin = this.authService.hasRole('sysadmin');
|
|
||||||
this.isGestor = this.authService.hasRole('gestor');
|
|
||||||
this.isFinanceiro = this.authService.hasRole('financeiro');
|
|
||||||
setTimeout(() => this.refresh());
|
setTimeout(() => this.refresh());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -170,90 +151,6 @@ export class TrocaNumero implements AfterViewInit {
|
||||||
this.loadForGroups();
|
this.loadForGroups();
|
||||||
}
|
}
|
||||||
|
|
||||||
async onExport(): Promise<void> {
|
|
||||||
if (this.exporting) return;
|
|
||||||
this.exporting = true;
|
|
||||||
|
|
||||||
try {
|
|
||||||
const rows = await this.fetchAllRowsForExport();
|
|
||||||
if (!rows.length) {
|
|
||||||
await this.showToast('Nenhum registro encontrado para exportar.');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const timestamp = this.tableExportService.buildTimestamp();
|
|
||||||
await this.tableExportService.exportAsXlsx<TrocaRow>({
|
|
||||||
fileName: `troca_numero_${timestamp}`,
|
|
||||||
sheetName: 'TrocaNumero',
|
|
||||||
rows,
|
|
||||||
columns: [
|
|
||||||
{ header: 'ID', value: (row) => row.id ?? '' },
|
|
||||||
{ header: 'Motivo', value: (row) => row.motivo },
|
|
||||||
{ header: 'Cliente', value: (row) => this.getRawField(row, ['cliente', 'Cliente']) ?? '' },
|
|
||||||
{ header: 'Usuario', value: (row) => this.getRawField(row, ['usuario', 'Usuario']) ?? '' },
|
|
||||||
{ header: 'Skil', value: (row) => this.getRawField(row, ['skil', 'Skil']) ?? '' },
|
|
||||||
{ header: 'Item', type: 'number', value: (row) => this.toNumberOrNull(row.item) ?? 0 },
|
|
||||||
{ header: 'Linha Antiga', value: (row) => row.linhaAntiga },
|
|
||||||
{ header: 'Linha Nova', value: (row) => row.linhaNova },
|
|
||||||
{ header: 'ICCID', value: (row) => row.iccid },
|
|
||||||
{ header: 'Data da Troca', type: 'date', value: (row) => row.dataTroca },
|
|
||||||
{ header: 'Observacao', value: (row) => row.observacao },
|
|
||||||
{ header: 'Situacao', value: (row) => (this.isTroca(row) ? 'TROCA' : 'SEM TROCA') },
|
|
||||||
{ header: 'Linha ID (Geral)', value: (row) => this.getRawField(row, ['mobileLineId', 'MobileLineId']) ?? '' },
|
|
||||||
{ header: 'Criado Em', type: 'datetime', value: (row) => this.getRawField(row, ['createdAt', 'CreatedAt']) ?? '' },
|
|
||||||
{ header: 'Atualizado Em', type: 'datetime', value: (row) => this.getRawField(row, ['updatedAt', 'UpdatedAt']) ?? '' },
|
|
||||||
],
|
|
||||||
});
|
|
||||||
|
|
||||||
await this.showToast(`Planilha exportada com ${rows.length} registro(s).`);
|
|
||||||
} catch {
|
|
||||||
await this.showToast('Erro ao exportar planilha.');
|
|
||||||
} finally {
|
|
||||||
this.exporting = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async fetchAllRowsForExport(): Promise<TrocaRow[]> {
|
|
||||||
const pageSize = 2000;
|
|
||||||
let page = 1;
|
|
||||||
let expectedTotal = 0;
|
|
||||||
const rows: TrocaRow[] = [];
|
|
||||||
|
|
||||||
while (page <= 500) {
|
|
||||||
const params = new HttpParams()
|
|
||||||
.set('page', String(page))
|
|
||||||
.set('pageSize', String(pageSize))
|
|
||||||
.set('search', (this.searchTerm ?? '').trim())
|
|
||||||
.set('sortBy', 'motivo')
|
|
||||||
.set('sortDir', 'asc');
|
|
||||||
|
|
||||||
const response = await firstValueFrom(
|
|
||||||
this.http.get<ApiPagedResult<any> | any[]>(this.apiBase, { params })
|
|
||||||
);
|
|
||||||
|
|
||||||
const items = Array.isArray(response) ? response : (response.items ?? []);
|
|
||||||
const normalized = items.map((item: any, idx: number) => this.normalizeRow(item, rows.length + idx));
|
|
||||||
rows.push(...normalized);
|
|
||||||
expectedTotal = Array.isArray(response) ? 0 : Number(response.total ?? 0);
|
|
||||||
|
|
||||||
if (Array.isArray(response)) break;
|
|
||||||
if (items.length === 0) break;
|
|
||||||
if (items.length < pageSize) break;
|
|
||||||
if (expectedTotal > 0 && rows.length >= expectedTotal) break;
|
|
||||||
page += 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
return rows.sort((a, b) => {
|
|
||||||
const byMotivo = (a.motivo ?? '').localeCompare(b.motivo ?? '', 'pt-BR', { sensitivity: 'base' });
|
|
||||||
if (byMotivo !== 0) return byMotivo;
|
|
||||||
|
|
||||||
const byItem = (this.toNumberOrNull(a.item) ?? 0) - (this.toNumberOrNull(b.item) ?? 0);
|
|
||||||
if (byItem !== 0) return byItem;
|
|
||||||
|
|
||||||
return (a.linhaNova ?? '').localeCompare(b.linhaNova ?? '', 'pt-BR', { sensitivity: 'base' });
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
onSearch() {
|
onSearch() {
|
||||||
if (this.searchTimer) clearTimeout(this.searchTimer);
|
if (this.searchTimer) clearTimeout(this.searchTimer);
|
||||||
this.searchTimer = setTimeout(() => {
|
this.searchTimer = setTimeout(() => {
|
||||||
|
|
@ -278,19 +175,29 @@ export class TrocaNumero implements AfterViewInit {
|
||||||
}
|
}
|
||||||
|
|
||||||
goToPage(p: number) {
|
goToPage(p: number) {
|
||||||
this.page = clampPage(p, this.totalPages);
|
this.page = Math.max(1, Math.min(this.totalPages, p));
|
||||||
this.applyPagination();
|
this.applyPagination();
|
||||||
}
|
}
|
||||||
|
|
||||||
get totalPages() { return computeTotalPages(this.total || 0, this.pageSize); }
|
get totalPages() { return Math.ceil((this.total || 0) / this.pageSize) || 1; }
|
||||||
|
|
||||||
get pageNumbers() {
|
get pageNumbers() {
|
||||||
return buildPageNumbers(this.page, this.totalPages);
|
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++) pages.push(i);
|
||||||
|
return pages;
|
||||||
}
|
}
|
||||||
|
|
||||||
get pageStart() { return computePageStart(this.total || 0, this.page, this.pageSize); }
|
get pageStart() { return this.total === 0 ? 0 : (this.page - 1) * this.pageSize + 1; }
|
||||||
get pageEnd() {
|
get pageEnd() {
|
||||||
return computePageEnd(this.total || 0, this.page, this.pageSize);
|
if (this.total === 0) return 0;
|
||||||
|
return Math.min(this.page * this.pageSize, this.total);
|
||||||
}
|
}
|
||||||
|
|
||||||
trackById(_: number, row: TrocaRow) { return row.id; }
|
trackById(_: number, row: TrocaRow) { return row.id; }
|
||||||
|
|
@ -507,11 +414,6 @@ export class TrocaNumero implements AfterViewInit {
|
||||||
|
|
||||||
// ====== MODAL EDIÇÃO ======
|
// ====== MODAL EDIÇÃO ======
|
||||||
onEditar(r: TrocaRow) {
|
onEditar(r: TrocaRow) {
|
||||||
if (!this.canManageRecords) {
|
|
||||||
this.showToast('Perfil Financeiro possui acesso somente leitura.');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.editOpen = true;
|
this.editOpen = true;
|
||||||
this.editSaving = false;
|
this.editSaving = false;
|
||||||
|
|
||||||
|
|
@ -534,11 +436,6 @@ export class TrocaNumero implements AfterViewInit {
|
||||||
}
|
}
|
||||||
|
|
||||||
saveEdit() {
|
saveEdit() {
|
||||||
if (!this.canManageRecords) {
|
|
||||||
this.showToast('Perfil Financeiro possui acesso somente leitura.');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!this.editModel || !this.editModel.id) return;
|
if (!this.editModel || !this.editModel.id) return;
|
||||||
this.editSaving = true;
|
this.editSaving = true;
|
||||||
|
|
||||||
|
|
@ -570,11 +467,6 @@ export class TrocaNumero implements AfterViewInit {
|
||||||
|
|
||||||
// ====== MODAL CRIAÇÃO ======
|
// ====== MODAL CRIAÇÃO ======
|
||||||
onCreate() {
|
onCreate() {
|
||||||
if (!this.canManageRecords) {
|
|
||||||
this.showToast('Perfil Financeiro possui acesso somente leitura.');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.createOpen = true;
|
this.createOpen = true;
|
||||||
this.createSaving = false;
|
this.createSaving = false;
|
||||||
|
|
||||||
|
|
@ -604,11 +496,6 @@ export class TrocaNumero implements AfterViewInit {
|
||||||
}
|
}
|
||||||
|
|
||||||
saveCreate() {
|
saveCreate() {
|
||||||
if (!this.canManageRecords) {
|
|
||||||
this.showToast('Perfil Financeiro possui acesso somente leitura.');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ✅ validações do "beber do GERAL"
|
// ✅ validações do "beber do GERAL"
|
||||||
if (!String(this.selectedCliente ?? '').trim()) {
|
if (!String(this.selectedCliente ?? '').trim()) {
|
||||||
this.showToast('Selecione um Cliente do GERAL.');
|
this.showToast('Selecione um Cliente do GERAL.');
|
||||||
|
|
@ -655,15 +542,6 @@ export class TrocaNumero implements AfterViewInit {
|
||||||
return Number.isFinite(n) ? n : null;
|
return Number.isFinite(n) ? n : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
private getRawField(row: TrocaRow, keys: string[]): string | null {
|
|
||||||
for (const key of keys) {
|
|
||||||
const value = row?.raw?.[key];
|
|
||||||
if (value === undefined || value === null || String(value).trim() === '') continue;
|
|
||||||
return String(value);
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
private isoToDateInput(iso: string | null | undefined): string {
|
private isoToDateInput(iso: string | null | undefined): string {
|
||||||
if (!iso) return '';
|
if (!iso) return '';
|
||||||
const dt = new Date(iso);
|
const dt = new Date(iso);
|
||||||
|
|
|
||||||
|
|
@ -24,10 +24,6 @@
|
||||||
<small class="subtitle">Controle de contratos e fidelização</small>
|
<small class="subtitle">Controle de contratos e fidelização</small>
|
||||||
</div>
|
</div>
|
||||||
<div class="header-actions d-flex gap-2 justify-content-end">
|
<div class="header-actions d-flex gap-2 justify-content-end">
|
||||||
<button class="btn btn-glass btn-sm" (click)="onExport()" [disabled]="loading || exporting">
|
|
||||||
<span *ngIf="!exporting"><i class="bi bi-download me-1"></i> Exportar</span>
|
|
||||||
<span *ngIf="exporting"><span class="spinner-border spinner-border-sm me-2"></span> Exportando...</span>
|
|
||||||
</button>
|
|
||||||
<button *ngIf="isSysAdmin" class="btn btn-brand btn-sm" (click)="openCreate()">
|
<button *ngIf="isSysAdmin" class="btn btn-brand btn-sm" (click)="openCreate()">
|
||||||
<i class="bi bi-plus-circle me-1"></i> Nova Vigência
|
<i class="bi bi-plus-circle me-1"></i> Nova Vigência
|
||||||
</button>
|
</button>
|
||||||
|
|
@ -192,4 +188,268 @@
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<app-vigencia-modals [vm]="$any(vm)"></app-vigencia-modals>
|
<div class="lg-backdrop" *ngIf="detailsOpen || editOpen || deleteOpen || createOpen" (click)="closeDetails(); closeEdit(); cancelDelete(); closeCreate()"></div>
|
||||||
|
|
||||||
|
<div class="lg-modal" *ngIf="detailsOpen">
|
||||||
|
<div class="lg-modal-card modal-lg" (click)="$event.stopPropagation()">
|
||||||
|
<div class="modal-header">
|
||||||
|
<div class="modal-title">
|
||||||
|
<span class="icon-bg primary-soft"><i class="bi bi-card-list"></i></span>
|
||||||
|
Detalhes da Vigência
|
||||||
|
</div>
|
||||||
|
<button class="btn-icon" (click)="closeDetails()"><i class="bi bi-x-lg"></i></button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="modal-body bg-light-gray">
|
||||||
|
<div class="details-dashboard">
|
||||||
|
<div class="detail-box">
|
||||||
|
<div class="box-header justify-content-center">
|
||||||
|
<span><i class="bi bi-card-text me-2"></i> Informações da Linha</span>
|
||||||
|
</div>
|
||||||
|
<div class="box-body">
|
||||||
|
<div class="info-grid">
|
||||||
|
<div class="info-item span-2">
|
||||||
|
<span class="lbl">Cliente</span>
|
||||||
|
<span class="val">{{ selectedRow?.cliente || '-' }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="info-item">
|
||||||
|
<span class="lbl">Linha</span>
|
||||||
|
<span class="val fw-black text-blue">{{ selectedRow?.linha || '-' }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="info-item">
|
||||||
|
<span class="lbl">Conta</span>
|
||||||
|
<span class="val">{{ selectedRow?.conta || '-' }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="info-item span-2">
|
||||||
|
<span class="lbl">Usuário</span>
|
||||||
|
<span class="val">{{ selectedRow?.usuario || '-' }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="info-item span-2">
|
||||||
|
<span class="lbl">Plano</span>
|
||||||
|
<span class="val">{{ selectedRow?.planoContrato || '-' }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="info-item">
|
||||||
|
<span class="lbl">Efetivação</span>
|
||||||
|
<span class="val">{{ selectedRow?.dtEfetivacaoServico ? (selectedRow?.dtEfetivacaoServico | date:'dd/MM/yyyy') : '-' }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="info-item">
|
||||||
|
<span class="lbl">Término</span>
|
||||||
|
<span class="val" [class.text-danger]="isVencido(selectedRow?.dtTerminoFidelizacao)">
|
||||||
|
{{ selectedRow?.dtTerminoFidelizacao ? (selectedRow?.dtTerminoFidelizacao | date:'dd/MM/yyyy') : '-' }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="info-item">
|
||||||
|
<span class="lbl">Situação</span>
|
||||||
|
<span class="status-pill" [class.is-danger]="isVencido(selectedRow?.dtTerminoFidelizacao)">
|
||||||
|
{{ isVencido(selectedRow?.dtTerminoFidelizacao) ? 'Vencido' : 'Ativo' }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="info-item">
|
||||||
|
<span class="lbl">Renovação</span>
|
||||||
|
<span class="val">
|
||||||
|
{{ selectedRow?.autoRenewYears ? ('Auto +' + selectedRow?.autoRenewYears + ' ano(s)') : 'Não programada' }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="info-item">
|
||||||
|
<span class="lbl">Valor Total</span>
|
||||||
|
<span class="val text-brand">{{ (selectedRow?.total || 0) | currency:'BRL' }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="modal-footer p-3 text-end border-top">
|
||||||
|
<button class="btn btn-glass btn-sm" (click)="closeDetails()">Fechar</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- CREATE MODAL -->
|
||||||
|
<div class="lg-modal" *ngIf="createOpen">
|
||||||
|
<div class="lg-modal-card modal-xl create-modal" (click)="$event.stopPropagation()">
|
||||||
|
<div class="modal-header">
|
||||||
|
<div class="modal-title">
|
||||||
|
<span class="icon-bg primary-soft"><i class="bi bi-plus-circle"></i></span>
|
||||||
|
Nova Vigência
|
||||||
|
</div>
|
||||||
|
<button class="btn-icon" (click)="closeCreate()"><i class="bi bi-x-lg"></i></button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="modal-body bg-light-gray">
|
||||||
|
<div class="edit-sections">
|
||||||
|
<details open class="detail-box">
|
||||||
|
<summary class="box-header">
|
||||||
|
<span><i class="bi bi-link-45deg me-2"></i> Vínculo com GERAL</span>
|
||||||
|
<i class="bi bi-chevron-down ms-auto transition-icon"></i>
|
||||||
|
</summary>
|
||||||
|
<div class="box-body">
|
||||||
|
<div class="form-grid">
|
||||||
|
<div class="form-field span-2">
|
||||||
|
<label>Cliente (GERAL)</label>
|
||||||
|
<app-select
|
||||||
|
class="form-select"
|
||||||
|
size="sm"
|
||||||
|
[options]="clientsFromGeral"
|
||||||
|
[(ngModel)]="createModel.selectedClient"
|
||||||
|
(ngModelChange)="onCreateClientChange()"
|
||||||
|
[disabled]="createClientsLoading"
|
||||||
|
></app-select>
|
||||||
|
</div>
|
||||||
|
<div class="form-field span-2">
|
||||||
|
<label>Linha (GERAL)</label>
|
||||||
|
<app-select
|
||||||
|
class="form-select"
|
||||||
|
size="sm"
|
||||||
|
[options]="lineOptionsCreate"
|
||||||
|
labelKey="label"
|
||||||
|
valueKey="id"
|
||||||
|
[(ngModel)]="createModel.mobileLineId"
|
||||||
|
(ngModelChange)="onCreateLineChange()"
|
||||||
|
[disabled]="createLinesLoading || !createModel.selectedClient"
|
||||||
|
></app-select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</details>
|
||||||
|
|
||||||
|
<details open class="detail-box">
|
||||||
|
<summary class="box-header">
|
||||||
|
<span><i class="bi bi-person-badge me-2"></i> Identificação</span>
|
||||||
|
<i class="bi bi-chevron-down ms-auto transition-icon"></i>
|
||||||
|
</summary>
|
||||||
|
<div class="box-body">
|
||||||
|
<div class="form-grid">
|
||||||
|
<div class="form-field"><label>Cliente</label><input class="form-control form-control-sm" [(ngModel)]="createModel.cliente" /></div>
|
||||||
|
<div class="form-field field-line"><label>Linha</label><input class="form-control form-control-sm" inputmode="numeric" [(ngModel)]="createModel.linha" /></div>
|
||||||
|
<div class="form-field"><label>Conta</label><input class="form-control form-control-sm" [(ngModel)]="createModel.conta" /></div>
|
||||||
|
<div class="form-field"><label>Usuário</label><input class="form-control form-control-sm" [(ngModel)]="createModel.usuario" /></div>
|
||||||
|
<div class="form-field span-2">
|
||||||
|
<label>Plano</label>
|
||||||
|
<app-select
|
||||||
|
*ngIf="planOptions.length > 0"
|
||||||
|
class="form-select"
|
||||||
|
size="sm"
|
||||||
|
[options]="planOptions"
|
||||||
|
[(ngModel)]="createModel.planoContrato"
|
||||||
|
(ngModelChange)="onCreatePlanChange()"
|
||||||
|
></app-select>
|
||||||
|
<input
|
||||||
|
*ngIf="planOptions.length === 0"
|
||||||
|
class="form-control form-control-sm"
|
||||||
|
[(ngModel)]="createModel.planoContrato"
|
||||||
|
(ngModelChange)="onCreatePlanChange()"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="form-field field-item field-auto">
|
||||||
|
<label>Item (Automático)</label>
|
||||||
|
<input class="form-control form-control-sm bg-light" type="number" [(ngModel)]="createModel.item" readonly title="Gerado automaticamente pelo sistema" />
|
||||||
|
<small class="field-hint">Gerado automaticamente pelo sistema</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</details>
|
||||||
|
|
||||||
|
<details open class="detail-box">
|
||||||
|
<summary class="box-header">
|
||||||
|
<span><i class="bi bi-calendar-event me-2"></i> Vigência e Valor</span>
|
||||||
|
<i class="bi bi-chevron-down ms-auto transition-icon"></i>
|
||||||
|
</summary>
|
||||||
|
<div class="box-body">
|
||||||
|
<div class="form-grid">
|
||||||
|
<div class="form-field"><label>Efetivação</label><input class="form-control form-control-sm" type="date" [(ngModel)]="createEfetivacao" /></div>
|
||||||
|
<div class="form-field"><label>Término</label><input class="form-control form-control-sm" type="date" [(ngModel)]="createTermino" /></div>
|
||||||
|
<div class="form-field span-2"><label>Total</label><input class="form-control form-control-sm" type="number" [(ngModel)]="createModel.total" /></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</details>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="modal-footer p-3 text-end border-top">
|
||||||
|
<button class="btn btn-glass btn-sm me-2" (click)="closeCreate()">Cancelar</button>
|
||||||
|
<button class="btn btn-brand btn-sm" [disabled]="createSaving" (click)="saveCreate()">
|
||||||
|
{{ createSaving ? 'Salvando...' : 'Salvar' }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- EDIT MODAL -->
|
||||||
|
<div class="lg-modal" *ngIf="editOpen">
|
||||||
|
<div class="lg-modal-card modal-xl" (click)="$event.stopPropagation()">
|
||||||
|
<div class="modal-header">
|
||||||
|
<div class="modal-title">
|
||||||
|
<span class="icon-bg primary-soft"><i class="bi bi-pencil-square"></i></span>
|
||||||
|
Editar Vigência
|
||||||
|
</div>
|
||||||
|
<button class="btn-icon" (click)="closeEdit()"><i class="bi bi-x-lg"></i></button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="modal-body bg-light-gray" *ngIf="editModel">
|
||||||
|
<div class="edit-sections">
|
||||||
|
<details open class="detail-box">
|
||||||
|
<summary class="box-header">
|
||||||
|
<span><i class="bi bi-person-badge me-2"></i> Identificação</span>
|
||||||
|
<i class="bi bi-chevron-down ms-auto transition-icon"></i>
|
||||||
|
</summary>
|
||||||
|
<div class="box-body">
|
||||||
|
<div class="form-grid">
|
||||||
|
<div class="form-field"><label>Cliente</label><input class="form-control form-control-sm" [(ngModel)]="editModel.cliente" /></div>
|
||||||
|
<div class="form-field field-line"><label>Linha</label><input class="form-control form-control-sm" inputmode="numeric" [(ngModel)]="editModel.linha" /></div>
|
||||||
|
<div class="form-field"><label>Conta</label><input class="form-control form-control-sm" [(ngModel)]="editModel.conta" /></div>
|
||||||
|
<div class="form-field"><label>Usuário</label><input class="form-control form-control-sm" [(ngModel)]="editModel.usuario" /></div>
|
||||||
|
<div class="form-field span-2"><label>Plano</label><input class="form-control form-control-sm" [(ngModel)]="editModel.planoContrato" (ngModelChange)="onEditPlanChange()" /></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</details>
|
||||||
|
|
||||||
|
<details open class="detail-box">
|
||||||
|
<summary class="box-header">
|
||||||
|
<span><i class="bi bi-calendar-event me-2"></i> Vigência e Valor</span>
|
||||||
|
<i class="bi bi-chevron-down ms-auto transition-icon"></i>
|
||||||
|
</summary>
|
||||||
|
<div class="box-body">
|
||||||
|
<div class="form-grid">
|
||||||
|
<div class="form-field"><label>Efetivação</label><input class="form-control form-control-sm" type="date" [(ngModel)]="editEfetivacao" /></div>
|
||||||
|
<div class="form-field"><label>Término</label><input class="form-control form-control-sm" type="date" [(ngModel)]="editTermino" /></div>
|
||||||
|
<div class="form-field span-2"><label>Total</label><input class="form-control form-control-sm" type="number" [(ngModel)]="editModel.total" /></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</details>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="modal-footer p-3 text-end border-top">
|
||||||
|
<button class="btn btn-glass btn-sm me-2" (click)="closeEdit()">Cancelar</button>
|
||||||
|
<button class="btn btn-primary btn-sm" [disabled]="editSaving" (click)="saveEdit()">
|
||||||
|
{{ editSaving ? 'Salvando...' : 'Salvar' }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- DELETE MODAL -->
|
||||||
|
<div class="lg-modal" *ngIf="deleteOpen">
|
||||||
|
<div class="lg-modal-card modal-lg" (click)="$event.stopPropagation()">
|
||||||
|
<div class="modal-header">
|
||||||
|
<div class="modal-title">
|
||||||
|
<span class="icon-bg danger-soft"><i class="bi bi-trash"></i></span>
|
||||||
|
Remover Vigência
|
||||||
|
</div>
|
||||||
|
<button class="btn-icon" (click)="cancelDelete()"><i class="bi bi-x-lg"></i></button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body bg-light-gray">
|
||||||
|
<div class="confirm-delete">
|
||||||
|
<div class="confirm-icon"><i class="bi bi-trash"></i></div>
|
||||||
|
<p class="mb-0">Confirma remover o registro <strong>{{ deleteTarget?.linha }}</strong>?</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer p-3 text-end border-top">
|
||||||
|
<button class="btn btn-glass btn-sm me-2" (click)="cancelDelete()">Cancelar</button>
|
||||||
|
<button class="btn btn-danger btn-sm" (click)="confirmDelete()">Excluir</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -372,6 +372,14 @@
|
||||||
.pagination-modern .page-item.active .page-link { background-color: var(--blue); border-color: var(--blue); color: #fff; }
|
.pagination-modern .page-item.active .page-link { background-color: var(--blue); border-color: var(--blue); color: #fff; }
|
||||||
|
|
||||||
/* MODAL */
|
/* MODAL */
|
||||||
|
.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 {
|
.lg-modal-card {
|
||||||
background: #ffffff;
|
background: #ffffff;
|
||||||
border: 1px solid rgba(255,255,255,0.86);
|
border: 1px solid rgba(255,255,255,0.86);
|
||||||
|
|
|
||||||
|
|
@ -3,16 +3,13 @@ import { CommonModule } from '@angular/common';
|
||||||
import { FormsModule } from '@angular/forms';
|
import { FormsModule } from '@angular/forms';
|
||||||
import { HttpErrorResponse } from '@angular/common/http';
|
import { HttpErrorResponse } from '@angular/common/http';
|
||||||
import { ActivatedRoute } from '@angular/router';
|
import { ActivatedRoute } from '@angular/router';
|
||||||
import { Subscription, firstValueFrom } from 'rxjs';
|
import { Subscription } from 'rxjs';
|
||||||
import { VigenciaService, VigenciaRow, VigenciaClientGroup, PagedResult, UpdateVigenciaRequest } from '../../services/vigencia.service';
|
import { VigenciaService, VigenciaRow, VigenciaClientGroup, PagedResult, UpdateVigenciaRequest } from '../../services/vigencia.service';
|
||||||
import { CustomSelectComponent } from '../../components/custom-select/custom-select';
|
import { CustomSelectComponent } from '../../components/custom-select/custom-select';
|
||||||
import { AuthService } from '../../services/auth.service';
|
import { AuthService } from '../../services/auth.service';
|
||||||
import { LinesService, MobileLineDetail } from '../../services/lines.service';
|
import { LinesService, MobileLineDetail } from '../../services/lines.service';
|
||||||
import { PlanAutoFillService } from '../../services/plan-autofill.service';
|
import { PlanAutoFillService } from '../../services/plan-autofill.service';
|
||||||
import { TableExportService } from '../../services/table-export.service';
|
|
||||||
import { confirmDeletionWithTyping } from '../../utils/destructive-confirmation';
|
import { confirmDeletionWithTyping } from '../../utils/destructive-confirmation';
|
||||||
import { computeTotalPages } from '../../utils/pagination.util';
|
|
||||||
import { VigenciaModalsComponent } from '../../components/page-modals/vigencia-modals/vigencia-modals';
|
|
||||||
|
|
||||||
type SortDir = 'asc' | 'desc';
|
type SortDir = 'asc' | 'desc';
|
||||||
type ToastType = 'success' | 'danger';
|
type ToastType = 'success' | 'danger';
|
||||||
|
|
@ -29,14 +26,12 @@ interface LineOptionDto {
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-vigencia',
|
selector: 'app-vigencia',
|
||||||
standalone: true,
|
standalone: true,
|
||||||
imports: [CommonModule, FormsModule, CustomSelectComponent, VigenciaModalsComponent],
|
imports: [CommonModule, FormsModule, CustomSelectComponent],
|
||||||
templateUrl: './vigencia.html',
|
templateUrl: './vigencia.html',
|
||||||
styleUrls: ['./vigencia.scss'],
|
styleUrls: ['./vigencia.scss'],
|
||||||
})
|
})
|
||||||
export class VigenciaComponent implements OnInit, OnDestroy {
|
export class VigenciaComponent implements OnInit, OnDestroy {
|
||||||
readonly vm = this;
|
|
||||||
loading = false;
|
loading = false;
|
||||||
exporting = false;
|
|
||||||
errorMsg = '';
|
errorMsg = '';
|
||||||
|
|
||||||
// Filtros
|
// Filtros
|
||||||
|
|
@ -118,8 +113,7 @@ export class VigenciaComponent implements OnInit, OnDestroy {
|
||||||
private authService: AuthService,
|
private authService: AuthService,
|
||||||
private linesService: LinesService,
|
private linesService: LinesService,
|
||||||
private planAutoFill: PlanAutoFillService,
|
private planAutoFill: PlanAutoFillService,
|
||||||
private route: ActivatedRoute,
|
private route: ActivatedRoute
|
||||||
private tableExportService: TableExportService
|
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
|
|
@ -163,7 +157,7 @@ export class VigenciaComponent implements OnInit, OnDestroy {
|
||||||
}
|
}
|
||||||
|
|
||||||
get totalPages(): number {
|
get totalPages(): number {
|
||||||
return computeTotalPages(this.total || 0, this.pageSize || 10);
|
return Math.max(1, Math.ceil((this.total || 0) / (this.pageSize || 10)));
|
||||||
}
|
}
|
||||||
|
|
||||||
fetch(goToPage?: number): void {
|
fetch(goToPage?: number): void {
|
||||||
|
|
@ -301,107 +295,6 @@ export class VigenciaComponent implements OnInit, OnDestroy {
|
||||||
this.fetch(1);
|
this.fetch(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
async onExport(): Promise<void> {
|
|
||||||
if (this.exporting) return;
|
|
||||||
this.exporting = true;
|
|
||||||
|
|
||||||
try {
|
|
||||||
const baseRows = await this.fetchAllRowsForExport();
|
|
||||||
const rows = await this.fetchDetailedRowsForExport(baseRows);
|
|
||||||
if (!rows.length) {
|
|
||||||
this.showToast('Nenhum registro encontrado para exportar.', 'danger');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const timestamp = this.tableExportService.buildTimestamp();
|
|
||||||
await this.tableExportService.exportAsXlsx<VigenciaRow>({
|
|
||||||
fileName: `vigencia_${timestamp}`,
|
|
||||||
sheetName: 'Vigencia',
|
|
||||||
rows,
|
|
||||||
columns: [
|
|
||||||
{ header: 'ID', value: (row) => row.id ?? '' },
|
|
||||||
{ header: 'Item', type: 'number', value: (row) => this.toNullableNumber(row.item) ?? 0 },
|
|
||||||
{ header: 'Linha', value: (row) => row.linha ?? '' },
|
|
||||||
{ header: 'Conta', value: (row) => row.conta ?? '' },
|
|
||||||
{ header: 'Cliente', value: (row) => row.cliente ?? '' },
|
|
||||||
{ header: 'Usuario', value: (row) => row.usuario ?? '' },
|
|
||||||
{ header: 'Plano', value: (row) => row.planoContrato ?? '' },
|
|
||||||
{ header: 'Efetivacao', type: 'date', value: (row) => row.dtEfetivacaoServico ?? '' },
|
|
||||||
{ header: 'Termino Fidelizacao', type: 'date', value: (row) => row.dtTerminoFidelizacao ?? '' },
|
|
||||||
{ header: 'Status', value: (row) => (this.isVencido(row.dtTerminoFidelizacao) ? 'Vencido' : 'Ativo') },
|
|
||||||
{ header: 'Auto Renovacao (anos)', type: 'number', value: (row) => this.toNullableNumber(row.autoRenewYears) ?? 0 },
|
|
||||||
{ header: 'Auto Renovacao Referencia', type: 'date', value: (row) => row.autoRenewReferenceEndDate ?? '' },
|
|
||||||
{ header: 'Auto Renovacao Configurada Em', type: 'datetime', value: (row) => row.autoRenewConfiguredAt ?? '' },
|
|
||||||
{ header: 'Ultima Auto Renovacao', type: 'datetime', value: (row) => row.lastAutoRenewedAt ?? '' },
|
|
||||||
{ header: 'Total', type: 'currency', value: (row) => this.toNullableNumber(row.total) ?? 0 },
|
|
||||||
{ header: 'Criado Em', type: 'datetime', value: (row) => row.createdAt ?? '' },
|
|
||||||
{ header: 'Atualizado Em', type: 'datetime', value: (row) => row.updatedAt ?? '' },
|
|
||||||
],
|
|
||||||
});
|
|
||||||
|
|
||||||
this.showToast(`Planilha exportada com ${rows.length} registro(s).`, 'success');
|
|
||||||
} catch {
|
|
||||||
this.showToast('Erro ao exportar planilha.', 'danger');
|
|
||||||
} finally {
|
|
||||||
this.exporting = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async fetchAllRowsForExport(): Promise<VigenciaRow[]> {
|
|
||||||
const pageSize = 500;
|
|
||||||
let page = 1;
|
|
||||||
let expectedTotal = 0;
|
|
||||||
const all: VigenciaRow[] = [];
|
|
||||||
|
|
||||||
while (page <= 500) {
|
|
||||||
const response = await firstValueFrom(
|
|
||||||
this.vigenciaService.getVigencia({
|
|
||||||
search: this.search?.trim(),
|
|
||||||
client: this.client?.trim(),
|
|
||||||
page,
|
|
||||||
pageSize,
|
|
||||||
sortBy: 'item',
|
|
||||||
sortDir: 'asc',
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
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 async fetchDetailedRowsForExport(rows: VigenciaRow[]): Promise<VigenciaRow[]> {
|
|
||||||
if (!rows.length) return [];
|
|
||||||
|
|
||||||
const detailedRows: VigenciaRow[] = [];
|
|
||||||
const chunkSize = 10;
|
|
||||||
|
|
||||||
for (let i = 0; i < rows.length; i += chunkSize) {
|
|
||||||
const chunk = rows.slice(i, i + chunkSize);
|
|
||||||
const resolved = await Promise.all(
|
|
||||||
chunk.map(async (row) => {
|
|
||||||
try {
|
|
||||||
return await firstValueFrom(this.vigenciaService.getById(row.id));
|
|
||||||
} catch {
|
|
||||||
return row;
|
|
||||||
}
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
detailedRows.push(...resolved);
|
|
||||||
}
|
|
||||||
|
|
||||||
return detailedRows;
|
|
||||||
}
|
|
||||||
|
|
||||||
scheduleAutoRenew(row: VigenciaRow): void {
|
scheduleAutoRenew(row: VigenciaRow): void {
|
||||||
if (!row?.id) return;
|
if (!row?.id) return;
|
||||||
const years = 2;
|
const years = 2;
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,6 @@ import { HttpClient } from '@angular/common/http';
|
||||||
import { environment } from '../../environments/environment';
|
import { environment } from '../../environments/environment';
|
||||||
import { BehaviorSubject } from 'rxjs';
|
import { BehaviorSubject } from 'rxjs';
|
||||||
import { tap } from 'rxjs/operators';
|
import { tap } from 'rxjs/operators';
|
||||||
import { MveAuditService } from './mve-audit.service';
|
|
||||||
|
|
||||||
export interface RegisterPayload {
|
export interface RegisterPayload {
|
||||||
name: string;
|
name: string;
|
||||||
|
|
@ -44,10 +43,7 @@ export class AuthService {
|
||||||
private readonly tokenExpiresAtKey = 'tokenExpiresAt';
|
private readonly tokenExpiresAtKey = 'tokenExpiresAt';
|
||||||
private readonly rememberMeHours = 6;
|
private readonly rememberMeHours = 6;
|
||||||
|
|
||||||
constructor(
|
constructor(private http: HttpClient) {
|
||||||
private http: HttpClient,
|
|
||||||
private readonly mveAuditService: MveAuditService
|
|
||||||
) {
|
|
||||||
this.syncUserProfileFromToken();
|
this.syncUserProfileFromToken();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -69,12 +65,10 @@ export class AuthService {
|
||||||
|
|
||||||
logout() {
|
logout() {
|
||||||
if (typeof window === 'undefined') {
|
if (typeof window === 'undefined') {
|
||||||
this.mveAuditService.clearCache();
|
|
||||||
this.userProfileSubject.next(null);
|
this.userProfileSubject.next(null);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.mveAuditService.clearCache();
|
|
||||||
this.clearTokenStorage(localStorage);
|
this.clearTokenStorage(localStorage);
|
||||||
this.clearTokenStorage(sessionStorage);
|
this.clearTokenStorage(sessionStorage);
|
||||||
this.userProfileSubject.next(null);
|
this.userProfileSubject.next(null);
|
||||||
|
|
@ -82,7 +76,6 @@ export class AuthService {
|
||||||
|
|
||||||
setToken(token: string, rememberMe = false) {
|
setToken(token: string, rememberMe = false) {
|
||||||
if (typeof window === 'undefined') return;
|
if (typeof window === 'undefined') return;
|
||||||
this.mveAuditService.clearCache();
|
|
||||||
this.clearTokenStorage(localStorage);
|
this.clearTokenStorage(localStorage);
|
||||||
this.clearTokenStorage(sessionStorage);
|
this.clearTokenStorage(sessionStorage);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,6 @@ import { Injectable } from '@angular/core';
|
||||||
import { HttpClient, HttpParams } from '@angular/common/http';
|
import { HttpClient, HttpParams } from '@angular/common/http';
|
||||||
import { Observable, map } from 'rxjs';
|
import { Observable, map } from 'rxjs';
|
||||||
import { environment } from '../../environments/environment';
|
import { environment } from '../../environments/environment';
|
||||||
import { buildApiBaseUrl } from '../utils/api-base.util';
|
|
||||||
|
|
||||||
export type SortDir = 'asc' | 'desc';
|
export type SortDir = 'asc' | 'desc';
|
||||||
export type TipoCliente = 'PF' | 'PJ';
|
export type TipoCliente = 'PF' | 'PJ';
|
||||||
|
|
@ -76,7 +75,8 @@ export class BillingService {
|
||||||
private readonly baseUrl: string;
|
private readonly baseUrl: string;
|
||||||
|
|
||||||
constructor(private http: HttpClient) {
|
constructor(private http: HttpClient) {
|
||||||
const apiBase = buildApiBaseUrl(environment.apiUrl);
|
const raw = (environment.apiUrl || '').replace(/\/+$/, '');
|
||||||
|
const apiBase = raw.toLowerCase().endsWith('/api') ? raw : `${raw}/api`;
|
||||||
this.baseUrl = `${apiBase}/billing`;
|
this.baseUrl = `${apiBase}/billing`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,6 @@ import { Injectable } from '@angular/core';
|
||||||
import { HttpClient, HttpParams } from '@angular/common/http';
|
import { HttpClient, HttpParams } from '@angular/common/http';
|
||||||
import { Observable } from 'rxjs';
|
import { Observable } from 'rxjs';
|
||||||
import { environment } from '../../environments/environment';
|
import { environment } from '../../environments/environment';
|
||||||
import { buildApiBaseUrl } from '../utils/api-base.util';
|
|
||||||
|
|
||||||
export type SortDir = 'asc' | 'desc';
|
export type SortDir = 'asc' | 'desc';
|
||||||
|
|
||||||
|
|
@ -72,7 +71,8 @@ export class ChipsControleService {
|
||||||
private readonly baseApi: string;
|
private readonly baseApi: string;
|
||||||
|
|
||||||
constructor(private http: HttpClient) {
|
constructor(private http: HttpClient) {
|
||||||
this.baseApi = buildApiBaseUrl(environment.apiUrl);
|
const raw = (environment.apiUrl || '').replace(/\/+$/, '');
|
||||||
|
this.baseApi = raw.toLowerCase().endsWith('/api') ? raw : `${raw}/api`;
|
||||||
}
|
}
|
||||||
|
|
||||||
getChipsVirgens(opts: {
|
getChipsVirgens(opts: {
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,6 @@ import { Injectable } from '@angular/core';
|
||||||
import { HttpClient, HttpParams } from '@angular/common/http';
|
import { HttpClient, HttpParams } from '@angular/common/http';
|
||||||
import { Observable } from 'rxjs';
|
import { Observable } from 'rxjs';
|
||||||
import { environment } from '../../environments/environment';
|
import { environment } from '../../environments/environment';
|
||||||
import { buildApiBaseUrl } from '../utils/api-base.util';
|
|
||||||
|
|
||||||
export type SortDir = 'asc' | 'desc';
|
export type SortDir = 'asc' | 'desc';
|
||||||
|
|
||||||
|
|
@ -76,7 +75,8 @@ export class DadosUsuariosService {
|
||||||
private readonly baseApi: string;
|
private readonly baseApi: string;
|
||||||
|
|
||||||
constructor(private http: HttpClient) {
|
constructor(private http: HttpClient) {
|
||||||
this.baseApi = buildApiBaseUrl(environment.apiUrl);
|
const raw = (environment.apiUrl || '').replace(/\/+$/, '');
|
||||||
|
this.baseApi = raw.toLowerCase().endsWith('/api') ? raw : `${raw}/api`;
|
||||||
}
|
}
|
||||||
|
|
||||||
getGroups(opts: {
|
getGroups(opts: {
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,6 @@ import { HttpClient, HttpParams } from '@angular/common/http';
|
||||||
import { Observable } from 'rxjs';
|
import { Observable } from 'rxjs';
|
||||||
|
|
||||||
import { environment } from '../../environments/environment';
|
import { environment } from '../../environments/environment';
|
||||||
import { buildApiBaseUrl } from '../utils/api-base.util';
|
|
||||||
|
|
||||||
export type AuditAction = 'CREATE' | 'UPDATE' | 'DELETE';
|
export type AuditAction = 'CREATE' | 'UPDATE' | 'DELETE';
|
||||||
export type AuditChangeType = 'added' | 'modified' | 'removed';
|
export type AuditChangeType = 'added' | 'modified' | 'removed';
|
||||||
|
|
@ -51,36 +50,13 @@ export interface HistoricoQuery {
|
||||||
pageSize?: number;
|
pageSize?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface LineHistoricoQuery {
|
|
||||||
line?: string;
|
|
||||||
pageName?: string;
|
|
||||||
action?: AuditAction | string;
|
|
||||||
user?: string;
|
|
||||||
search?: string;
|
|
||||||
dateFrom?: string;
|
|
||||||
dateTo?: string;
|
|
||||||
page?: number;
|
|
||||||
pageSize?: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ChipHistoricoQuery {
|
|
||||||
chip?: string;
|
|
||||||
pageName?: string;
|
|
||||||
action?: AuditAction | string;
|
|
||||||
user?: string;
|
|
||||||
search?: string;
|
|
||||||
dateFrom?: string;
|
|
||||||
dateTo?: string;
|
|
||||||
page?: number;
|
|
||||||
pageSize?: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Injectable({ providedIn: 'root' })
|
@Injectable({ providedIn: 'root' })
|
||||||
export class HistoricoService {
|
export class HistoricoService {
|
||||||
private readonly baseApi: string;
|
private readonly baseApi: string;
|
||||||
|
|
||||||
constructor(private http: HttpClient) {
|
constructor(private http: HttpClient) {
|
||||||
this.baseApi = buildApiBaseUrl(environment.apiUrl);
|
const raw = (environment.apiUrl || '').replace(/\/+$/, '');
|
||||||
|
this.baseApi = raw.toLowerCase().endsWith('/api') ? raw : `${raw}/api`;
|
||||||
}
|
}
|
||||||
|
|
||||||
list(params: HistoricoQuery): Observable<PagedResult<AuditLogDto>> {
|
list(params: HistoricoQuery): Observable<PagedResult<AuditLogDto>> {
|
||||||
|
|
@ -98,36 +74,4 @@ export class HistoricoService {
|
||||||
|
|
||||||
return this.http.get<PagedResult<AuditLogDto>>(`${this.baseApi}/historico`, { params: httpParams });
|
return this.http.get<PagedResult<AuditLogDto>>(`${this.baseApi}/historico`, { params: httpParams });
|
||||||
}
|
}
|
||||||
|
|
||||||
listByLine(params: LineHistoricoQuery): Observable<PagedResult<AuditLogDto>> {
|
|
||||||
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<PagedResult<AuditLogDto>>(`${this.baseApi}/historico/linhas`, { params: httpParams });
|
|
||||||
}
|
|
||||||
|
|
||||||
listByChip(params: ChipHistoricoQuery): Observable<PagedResult<AuditLogDto>> {
|
|
||||||
let httpParams = new HttpParams();
|
|
||||||
if (params.chip) httpParams = httpParams.set('chip', params.chip);
|
|
||||||
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<PagedResult<AuditLogDto>>(`${this.baseApi}/historico/chips`, { params: httpParams });
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,6 @@ import { HttpClient, HttpParams } from '@angular/common/http';
|
||||||
import { Injectable } from '@angular/core';
|
import { Injectable } from '@angular/core';
|
||||||
import { Observable } from 'rxjs';
|
import { Observable } from 'rxjs';
|
||||||
import { environment } from '../../environments/environment';
|
import { environment } from '../../environments/environment';
|
||||||
import { buildApiBaseUrl } from '../utils/api-base.util';
|
|
||||||
|
|
||||||
export interface PagedResult<T> {
|
export interface PagedResult<T> {
|
||||||
page: number;
|
page: number;
|
||||||
|
|
@ -19,10 +18,6 @@ export interface MobileLineList {
|
||||||
chip: string | null;
|
chip: string | null;
|
||||||
cliente: string | null;
|
cliente: string | null;
|
||||||
usuario: string | null;
|
usuario: string | null;
|
||||||
centroDeCustos?: string | null;
|
|
||||||
setorNome?: string | null;
|
|
||||||
aparelhoNome?: string | null;
|
|
||||||
aparelhoCor?: string | null;
|
|
||||||
planoContrato: string | null;
|
planoContrato: string | null;
|
||||||
status: string | null;
|
status: string | null;
|
||||||
skil: string | null;
|
skil: string | null;
|
||||||
|
|
@ -31,12 +26,6 @@ export interface MobileLineList {
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface MobileLineDetail extends MobileLineList {
|
export interface MobileLineDetail extends MobileLineList {
|
||||||
setorId?: string | null;
|
|
||||||
aparelhoId?: string | null;
|
|
||||||
aparelhoImei?: string | null;
|
|
||||||
aparelhoNotaFiscalTemArquivo?: boolean;
|
|
||||||
aparelhoReciboTemArquivo?: boolean;
|
|
||||||
|
|
||||||
franquiaVivo?: number | null;
|
franquiaVivo?: number | null;
|
||||||
valorPlanoVivo?: number | null;
|
valorPlanoVivo?: number | null;
|
||||||
gestaoVozDados?: number | null;
|
gestaoVozDados?: number | null;
|
||||||
|
|
@ -73,7 +62,8 @@ export class LinesService {
|
||||||
private readonly baseUrl: string;
|
private readonly baseUrl: string;
|
||||||
|
|
||||||
constructor(private http: HttpClient) {
|
constructor(private http: HttpClient) {
|
||||||
const apiBase = buildApiBaseUrl(environment.apiUrl);
|
const raw = (environment.apiUrl || '').replace(/\/+$/, '');
|
||||||
|
const apiBase = raw.toLowerCase().endsWith('/api') ? raw : `${raw}/api`;
|
||||||
this.baseUrl = `${apiBase}/lines`;
|
this.baseUrl = `${apiBase}/lines`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,189 +0,0 @@
|
||||||
import { HttpClient } from '@angular/common/http';
|
|
||||||
import { Injectable } from '@angular/core';
|
|
||||||
import { Observable, of } from 'rxjs';
|
|
||||||
import { catchError, tap } from 'rxjs/operators';
|
|
||||||
import { environment } from '../../environments/environment';
|
|
||||||
import { buildApiBaseUrl } from '../utils/api-base.util';
|
|
||||||
|
|
||||||
export const MVE_AUDIT_CURRENT_RUN_ID_STORAGE_KEY = 'linegestao.mveAudit.currentRunId';
|
|
||||||
|
|
||||||
export interface MveAuditSummary {
|
|
||||||
totalSystemLines: number;
|
|
||||||
totalReportLines: number;
|
|
||||||
totalConciliated: number;
|
|
||||||
totalStatusDivergences: number;
|
|
||||||
totalDataDivergences: number;
|
|
||||||
totalOnlyInSystem: number;
|
|
||||||
totalOnlyInReport: number;
|
|
||||||
totalDuplicateReportLines: number;
|
|
||||||
totalDuplicateSystemLines: number;
|
|
||||||
totalInvalidRows: number;
|
|
||||||
totalUnknownStatuses: number;
|
|
||||||
totalSyncableIssues: number;
|
|
||||||
appliedIssuesCount: number;
|
|
||||||
appliedLinesCount: number;
|
|
||||||
appliedFieldsCount: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface MveAuditSnapshot {
|
|
||||||
numeroLinha?: string | null;
|
|
||||||
statusLinha?: string | null;
|
|
||||||
statusConta?: string | null;
|
|
||||||
planoLinha?: string | null;
|
|
||||||
dataAtivacao?: string | null;
|
|
||||||
terminoContrato?: string | null;
|
|
||||||
chip?: string | null;
|
|
||||||
conta?: string | null;
|
|
||||||
cnpj?: string | null;
|
|
||||||
modeloAparelho?: string | null;
|
|
||||||
fabricante?: string | null;
|
|
||||||
servicosAtivos?: string[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface MveAuditDifference {
|
|
||||||
fieldKey: string;
|
|
||||||
label: string;
|
|
||||||
systemValue?: string | null;
|
|
||||||
reportValue?: string | null;
|
|
||||||
syncable: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface MveAuditIssue {
|
|
||||||
id: string;
|
|
||||||
sourceRowNumber?: number | null;
|
|
||||||
numeroLinha: string;
|
|
||||||
mobileLineId?: string | null;
|
|
||||||
systemItem?: number | null;
|
|
||||||
issueType: string;
|
|
||||||
situation: string;
|
|
||||||
severity: string;
|
|
||||||
syncable: boolean;
|
|
||||||
applied: boolean;
|
|
||||||
actionSuggestion?: string | null;
|
|
||||||
notes?: string | null;
|
|
||||||
systemStatus?: string | null;
|
|
||||||
reportStatus?: string | null;
|
|
||||||
systemPlan?: string | null;
|
|
||||||
reportPlan?: string | null;
|
|
||||||
systemSnapshot?: MveAuditSnapshot | null;
|
|
||||||
reportSnapshot?: MveAuditSnapshot | null;
|
|
||||||
differences: MveAuditDifference[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface MveAuditRun {
|
|
||||||
id: string;
|
|
||||||
fileName?: string | null;
|
|
||||||
fileEncoding?: string | null;
|
|
||||||
status: string;
|
|
||||||
importedAtUtc: string;
|
|
||||||
appliedAtUtc?: string | null;
|
|
||||||
appliedByUserName?: string | null;
|
|
||||||
appliedByUserEmail?: string | null;
|
|
||||||
summary: MveAuditSummary;
|
|
||||||
issues: MveAuditIssue[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ApplyMveAuditResult {
|
|
||||||
auditRunId: string;
|
|
||||||
requestedIssues: number;
|
|
||||||
appliedIssues: number;
|
|
||||||
updatedLines: number;
|
|
||||||
updatedFields: number;
|
|
||||||
skippedIssues: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Injectable({ providedIn: 'root' })
|
|
||||||
export class MveAuditService {
|
|
||||||
private readonly baseUrl: string;
|
|
||||||
private currentRun: MveAuditRun | null = null;
|
|
||||||
|
|
||||||
constructor(private readonly http: HttpClient) {
|
|
||||||
const apiBase = buildApiBaseUrl(environment.apiUrl);
|
|
||||||
this.baseUrl = `${apiBase}/mve-audit`;
|
|
||||||
}
|
|
||||||
|
|
||||||
preview(file: File): Observable<MveAuditRun> {
|
|
||||||
const form = new FormData();
|
|
||||||
form.append('file', file);
|
|
||||||
return this.http
|
|
||||||
.post<MveAuditRun>(`${this.baseUrl}/preview`, form)
|
|
||||||
.pipe(tap((run) => this.cacheRun(run)));
|
|
||||||
}
|
|
||||||
|
|
||||||
getById(id: string): Observable<MveAuditRun> {
|
|
||||||
return this.http
|
|
||||||
.get<MveAuditRun>(`${this.baseUrl}/${id}`)
|
|
||||||
.pipe(tap((run) => this.cacheRun(run)));
|
|
||||||
}
|
|
||||||
|
|
||||||
getLatest(): Observable<MveAuditRun> {
|
|
||||||
return this.http
|
|
||||||
.get<MveAuditRun>(`${this.baseUrl}/latest`)
|
|
||||||
.pipe(tap((run) => this.cacheRun(run)));
|
|
||||||
}
|
|
||||||
|
|
||||||
apply(runId: string, issueIds?: string[]): Observable<ApplyMveAuditResult> {
|
|
||||||
return this.http.post<ApplyMveAuditResult>(`${this.baseUrl}/${runId}/apply`, {
|
|
||||||
issueIds: issueIds && issueIds.length > 0 ? issueIds : null,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
getCachedRun(): MveAuditRun | null {
|
|
||||||
return this.currentRun;
|
|
||||||
}
|
|
||||||
|
|
||||||
getCachedRunId(): string | null {
|
|
||||||
if (this.currentRun?.id) {
|
|
||||||
return this.currentRun.id;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (typeof window === 'undefined') {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return sessionStorage.getItem(MVE_AUDIT_CURRENT_RUN_ID_STORAGE_KEY);
|
|
||||||
}
|
|
||||||
|
|
||||||
restoreCachedRun(): Observable<MveAuditRun | null> {
|
|
||||||
if (this.currentRun) {
|
|
||||||
return of(this.currentRun);
|
|
||||||
}
|
|
||||||
|
|
||||||
const runId = this.getCachedRunId();
|
|
||||||
if (!runId) {
|
|
||||||
return of(null);
|
|
||||||
}
|
|
||||||
|
|
||||||
return this.getById(runId).pipe(
|
|
||||||
catchError(() => {
|
|
||||||
this.clearCache();
|
|
||||||
return of(null);
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
clearCache(): void {
|
|
||||||
this.currentRun = null;
|
|
||||||
|
|
||||||
if (typeof window === 'undefined') {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
sessionStorage.removeItem(MVE_AUDIT_CURRENT_RUN_ID_STORAGE_KEY);
|
|
||||||
}
|
|
||||||
|
|
||||||
private cacheRun(run: MveAuditRun | null | undefined): void {
|
|
||||||
if (!run?.id) {
|
|
||||||
this.clearCache();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.currentRun = run;
|
|
||||||
|
|
||||||
if (typeof window === 'undefined') {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
sessionStorage.setItem(MVE_AUDIT_CURRENT_RUN_ID_STORAGE_KEY, run.id);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -3,7 +3,6 @@ import { HttpClient, HttpParams, HttpResponse } from '@angular/common/http';
|
||||||
import { Observable, Subject, tap } from 'rxjs';
|
import { Observable, Subject, tap } from 'rxjs';
|
||||||
|
|
||||||
import { environment } from '../../environments/environment';
|
import { environment } from '../../environments/environment';
|
||||||
import { buildApiBaseUrl } from '../utils/api-base.util';
|
|
||||||
|
|
||||||
export type NotificationTipo = 'AVencer' | 'Vencido' | 'RenovacaoAutomatica' | string;
|
export type NotificationTipo = 'AVencer' | 'Vencido' | 'RenovacaoAutomatica' | string;
|
||||||
|
|
||||||
|
|
@ -41,7 +40,8 @@ export class NotificationsService {
|
||||||
readonly events$ = this.eventsSubject.asObservable();
|
readonly events$ = this.eventsSubject.asObservable();
|
||||||
|
|
||||||
constructor(private http: HttpClient) {
|
constructor(private http: HttpClient) {
|
||||||
this.baseApi = buildApiBaseUrl(environment.apiUrl);
|
const raw = (environment.apiUrl || '').replace(/\/+$/, '');
|
||||||
|
this.baseApi = raw.toLowerCase().endsWith('/api') ? raw : `${raw}/api`;
|
||||||
}
|
}
|
||||||
|
|
||||||
list(): Observable<NotificationDto[]> {
|
list(): Observable<NotificationDto[]> {
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,6 @@ import { Injectable } from '@angular/core';
|
||||||
import { HttpClient, HttpParams } from '@angular/common/http';
|
import { HttpClient, HttpParams } from '@angular/common/http';
|
||||||
import { Observable } from 'rxjs';
|
import { Observable } from 'rxjs';
|
||||||
import { environment } from '../../environments/environment';
|
import { environment } from '../../environments/environment';
|
||||||
import { buildApiBaseUrl } from '../utils/api-base.util';
|
|
||||||
|
|
||||||
export interface PagedResult<T> {
|
export interface PagedResult<T> {
|
||||||
page: number;
|
page: number;
|
||||||
|
|
@ -77,7 +76,8 @@ export class ParcelamentosService {
|
||||||
private readonly baseApi: string;
|
private readonly baseApi: string;
|
||||||
|
|
||||||
constructor(private http: HttpClient) {
|
constructor(private http: HttpClient) {
|
||||||
this.baseApi = buildApiBaseUrl(environment.apiUrl);
|
const raw = (environment.apiUrl || '').replace(/\/+$/, '');
|
||||||
|
this.baseApi = raw.toLowerCase().endsWith('/api') ? raw : `${raw}/api`;
|
||||||
}
|
}
|
||||||
|
|
||||||
list(filters: {
|
list(filters: {
|
||||||
|
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue