Compare commits
No commits in common. "f78f1c891ec11b6e3a01ff38aec2433e24db5a3e" and "ec3abc056fc537a0c4cc37fd55436448b989b0d5" have entirely different histories.
f78f1c891e
...
ec3abc056f
|
|
@ -9,7 +9,6 @@ import { Faturamento } from './pages/faturamento/faturamento';
|
||||||
|
|
||||||
import { authGuard } from './guards/auth.guard';
|
import { authGuard } from './guards/auth.guard';
|
||||||
import { adminGuard } from './guards/admin.guard';
|
import { adminGuard } from './guards/admin.guard';
|
||||||
import { systemAdminGuard } from './guards/system-admin.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';
|
||||||
import { TrocaNumero } from './pages/troca-numero/troca-numero';
|
import { TrocaNumero } from './pages/troca-numero/troca-numero';
|
||||||
|
|
@ -20,7 +19,6 @@ 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 { Perfil } from './pages/perfil/perfil';
|
import { Perfil } from './pages/perfil/perfil';
|
||||||
import { SystemProvisionUserPage } from './pages/system-provision-user/system-provision-user';
|
|
||||||
|
|
||||||
export const routes: Routes = [
|
export const routes: Routes = [
|
||||||
{ path: '', component: Home },
|
{ path: '', component: Home },
|
||||||
|
|
@ -39,12 +37,6 @@ export const routes: Routes = [
|
||||||
{ path: 'parcelamentos', component: Parcelamentos, canActivate: [authGuard, adminGuard], title: 'Parcelamentos' },
|
{ path: 'parcelamentos', component: Parcelamentos, canActivate: [authGuard, adminGuard], title: 'Parcelamentos' },
|
||||||
{ path: 'historico', component: Historico, canActivate: [authGuard, adminGuard], title: 'Histórico' },
|
{ path: 'historico', component: Historico, canActivate: [authGuard, adminGuard], title: 'Histórico' },
|
||||||
{ path: 'perfil', component: Perfil, canActivate: [authGuard], title: 'Perfil' },
|
{ path: 'perfil', component: Perfil, canActivate: [authGuard], title: 'Perfil' },
|
||||||
{
|
|
||||||
path: 'system/fornecer-usuario',
|
|
||||||
component: SystemProvisionUserPage,
|
|
||||||
canActivate: [authGuard, systemAdminGuard],
|
|
||||||
title: 'Fornecer Usuário',
|
|
||||||
},
|
|
||||||
|
|
||||||
// ✅ rota correta
|
// ✅ rota correta
|
||||||
{ path: 'dashboard', component: Dashboard, canActivate: [authGuard], title: 'Dashboard' },
|
{ path: 'dashboard', component: Dashboard, canActivate: [authGuard], title: 'Dashboard' },
|
||||||
|
|
|
||||||
|
|
@ -41,7 +41,6 @@ export class AppComponent {
|
||||||
'/parcelamentos',
|
'/parcelamentos',
|
||||||
'/historico',
|
'/historico',
|
||||||
'/perfil',
|
'/perfil',
|
||||||
'/system',
|
|
||||||
];
|
];
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
|
|
|
||||||
|
|
@ -191,9 +191,6 @@
|
||||||
<button type="button" class="options-item" *ngIf="isAdmin" (click)="openManageUsersModal()">
|
<button type="button" class="options-item" *ngIf="isAdmin" (click)="openManageUsersModal()">
|
||||||
<i class="bi bi-people"></i> Editar usuário
|
<i class="bi bi-people"></i> Editar usuário
|
||||||
</button>
|
</button>
|
||||||
<button type="button" class="options-item" *ngIf="isSystemAdmin" (click)="goToSystemProvisionUser()">
|
|
||||||
<i class="bi bi-shield-lock"></i> Fornecer usuário (cliente)
|
|
||||||
</button>
|
|
||||||
<div class="divider"></div>
|
<div class="divider"></div>
|
||||||
<button type="button" class="options-item danger" (click)="logout()">
|
<button type="button" class="options-item danger" (click)="logout()">
|
||||||
<i class="bi bi-box-arrow-right"></i> Sair
|
<i class="bi bi-box-arrow-right"></i> Sair
|
||||||
|
|
@ -507,38 +504,35 @@
|
||||||
<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>
|
||||||
<a *ngIf="canViewAll" routerLink="/resumo" routerLinkActive="active" class="side-item" (click)="closeMenu()">
|
<a routerLink="/resumo" routerLinkActive="active" class="side-item" (click)="closeMenu()">
|
||||||
<i class="bi bi-table"></i> <span>Resumo</span>
|
<i class="bi bi-table"></i> <span>Resumo</span>
|
||||||
</a>
|
</a>
|
||||||
<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="canViewAll" routerLink="/mureg" routerLinkActive="active" class="side-item" (click)="closeMenu()">
|
<a 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="canViewAll" routerLink="/faturamento" routerLinkActive="active" class="side-item" (click)="closeMenu()">
|
<a *ngIf="isAdmin" 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="canViewAll" routerLink="/parcelamentos" routerLinkActive="active" class="side-item" (click)="closeMenu()">
|
<a *ngIf="isAdmin" 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="isAdmin" 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="/dadosusuarios" routerLinkActive="active" class="side-item" (click)="closeMenu()">
|
<a 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>
|
||||||
<a *ngIf="canViewAll" routerLink="/vigencia" routerLinkActive="active" class="side-item" (click)="closeMenu()">
|
<a routerLink="/vigencia" routerLinkActive="active" class="side-item" (click)="closeMenu()">
|
||||||
<i class="bi bi-calendar2-check-fill"></i> <span>Vigência</span>
|
<i class="bi bi-calendar2-check-fill"></i> <span>Vigência</span>
|
||||||
</a>
|
</a>
|
||||||
<a *ngIf="canViewAll" routerLink="/chips-controle-recebidos" routerLinkActive="active" class="side-item" (click)="closeMenu()">
|
<a *ngIf="isAdmin" routerLink="/chips-controle-recebidos" routerLinkActive="active" class="side-item" (click)="closeMenu()">
|
||||||
<i class="bi bi-archive-fill"></i> <span>Chips Virgens e Recebidos</span>
|
<i class="bi bi-archive-fill"></i> <span>Chips Virgens e Recebidos</span>
|
||||||
</a>
|
</a>
|
||||||
<a *ngIf="canViewAll" routerLink="/trocanumero" routerLinkActive="active" class="side-item" (click)="closeMenu()">
|
<a routerLink="/trocanumero" routerLinkActive="active" class="side-item" (click)="closeMenu()">
|
||||||
<i class="bi bi-arrow-left-right"></i> <span>Troca de número</span>
|
<i class="bi bi-arrow-left-right"></i> <span>Troca de número</span>
|
||||||
</a>
|
</a>
|
||||||
<a *ngIf="isSystemAdmin" routerLink="/system/fornecer-usuario" routerLinkActive="active" class="side-item" (click)="closeMenu()">
|
|
||||||
<i class="bi bi-shield-lock-fill"></i> <span>Fornecer usuário</span>
|
|
||||||
</a>
|
|
||||||
</div>
|
</div>
|
||||||
</aside>
|
</aside>
|
||||||
|
|
|
||||||
|
|
@ -32,8 +32,6 @@ export class Header implements AfterViewInit, OnDestroy {
|
||||||
isLoggedHeader = false;
|
isLoggedHeader = false;
|
||||||
isHome = false;
|
isHome = false;
|
||||||
isAdmin = false;
|
isAdmin = false;
|
||||||
canViewAll = false;
|
|
||||||
isSystemAdmin = false;
|
|
||||||
notifications: NotificationDto[] = [];
|
notifications: NotificationDto[] = [];
|
||||||
notificationsLoading = false;
|
notificationsLoading = false;
|
||||||
notificationsError = false;
|
notificationsError = false;
|
||||||
|
|
@ -54,9 +52,8 @@ export class Header implements AfterViewInit, OnDestroy {
|
||||||
createUserForbidden = false;
|
createUserForbidden = false;
|
||||||
createUserSuccess = '';
|
createUserSuccess = '';
|
||||||
readonly permissionOptions = [
|
readonly permissionOptions = [
|
||||||
{ value: 'sysadmin', label: 'SysAdmin' },
|
{ value: 'admin', label: 'Administrador' },
|
||||||
{ value: 'gestor', label: 'Gestor' },
|
{ value: 'gestor', label: 'Gestor' },
|
||||||
{ value: 'cliente', label: 'Cliente' },
|
|
||||||
];
|
];
|
||||||
|
|
||||||
manageUsersLoading = false;
|
manageUsersLoading = false;
|
||||||
|
|
@ -89,7 +86,6 @@ export class Header implements AfterViewInit, OnDestroy {
|
||||||
'/parcelamentos',
|
'/parcelamentos',
|
||||||
'/historico',
|
'/historico',
|
||||||
'/perfil',
|
'/perfil',
|
||||||
'/system',
|
|
||||||
];
|
];
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
|
|
@ -204,15 +200,9 @@ export class Header implements AfterViewInit, OnDestroy {
|
||||||
private syncPermissions() {
|
private syncPermissions() {
|
||||||
if (!isPlatformBrowser(this.platformId)) {
|
if (!isPlatformBrowser(this.platformId)) {
|
||||||
this.isAdmin = false;
|
this.isAdmin = false;
|
||||||
this.canViewAll = false;
|
|
||||||
this.isSystemAdmin = false;
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const isSysAdmin = this.authService.hasRole('sysadmin');
|
this.isAdmin = this.authService.hasRole('admin');
|
||||||
const isGestor = this.authService.hasRole('gestor');
|
|
||||||
this.isAdmin = isSysAdmin;
|
|
||||||
this.canViewAll = isSysAdmin || isGestor;
|
|
||||||
this.isSystemAdmin = this.authService.hasRole('sysadmin');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
toggleMenu() {
|
toggleMenu() {
|
||||||
|
|
@ -237,12 +227,6 @@ export class Header implements AfterViewInit, OnDestroy {
|
||||||
this.router.navigate(['/perfil']);
|
this.router.navigate(['/perfil']);
|
||||||
}
|
}
|
||||||
|
|
||||||
goToSystemProvisionUser() {
|
|
||||||
if (!this.isSystemAdmin) return;
|
|
||||||
this.closeOptions();
|
|
||||||
this.router.navigate(['/system/fornecer-usuario']);
|
|
||||||
}
|
|
||||||
|
|
||||||
openCreateUserModal() {
|
openCreateUserModal() {
|
||||||
if (!this.isAdmin) return;
|
if (!this.isAdmin) return;
|
||||||
this.createUserOpen = true;
|
this.createUserOpen = true;
|
||||||
|
|
@ -446,8 +430,6 @@ export class Header implements AfterViewInit, OnDestroy {
|
||||||
this.optionsOpen = false;
|
this.optionsOpen = false;
|
||||||
this.notificationsOpen = false;
|
this.notificationsOpen = false;
|
||||||
this.isAdmin = false;
|
this.isAdmin = false;
|
||||||
this.canViewAll = false;
|
|
||||||
this.isSystemAdmin = false;
|
|
||||||
this.router.navigate(['/']);
|
this.router.navigate(['/']);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -18,8 +18,8 @@ export const adminGuard: CanActivateFn = () => {
|
||||||
return router.parseUrl('/login');
|
return router.parseUrl('/login');
|
||||||
}
|
}
|
||||||
|
|
||||||
const hasAccess = authService.hasRole('sysadmin') || authService.hasRole('gestor');
|
const isAdmin = authService.hasRole('admin');
|
||||||
if (!hasAccess) {
|
if (!isAdmin) {
|
||||||
return router.parseUrl('/dashboard');
|
return router.parseUrl('/dashboard');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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 systemAdminGuard: CanActivateFn = () => {
|
|
||||||
const router = inject(Router);
|
|
||||||
const platformId = inject(PLATFORM_ID);
|
|
||||||
const authService = inject(AuthService);
|
|
||||||
|
|
||||||
if (!isPlatformBrowser(platformId)) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
const token = authService.token;
|
|
||||||
if (!token) {
|
|
||||||
return router.parseUrl('/login');
|
|
||||||
}
|
|
||||||
|
|
||||||
const isSystemAdmin = authService.hasRole('sysadmin');
|
|
||||||
if (!isSystemAdmin) {
|
|
||||||
return router.parseUrl('/dashboard');
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
};
|
|
||||||
|
|
@ -129,7 +129,7 @@ export class ChipsControleRecebidos implements OnInit, OnDestroy {
|
||||||
|
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
if (!isPlatformBrowser(this.platformId)) return;
|
if (!isPlatformBrowser(this.platformId)) return;
|
||||||
this.isAdmin = this.authService.hasRole('sysadmin');
|
this.isAdmin = this.authService.hasRole('admin');
|
||||||
this.fetchChips();
|
this.fetchChips();
|
||||||
this.fetchControle();
|
this.fetchControle();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -117,7 +117,7 @@ export class DadosUsuarios implements OnInit {
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
this.isAdmin = this.authService.hasRole('sysadmin');
|
this.isAdmin = this.authService.hasRole('admin');
|
||||||
this.fetch(1);
|
this.fetch(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -40,331 +40,288 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<ng-container *ngIf="!isCliente; else clienteDashboard">
|
<div class="context-title fade-in-up" [style.animation-delay]="'180ms'">
|
||||||
<div class="context-title fade-in-up" [style.animation-delay]="'180ms'">
|
<h2>Página Geral</h2>
|
||||||
<h2>Página Geral</h2>
|
<p>Distribuição e saúde atual da base de linhas.</p>
|
||||||
<p>Distribuição e saúde atual da base de linhas.</p>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="dashboard-section fade-in-up" [style.animation-delay]="'220ms'">
|
<div class="dashboard-section fade-in-up" [style.animation-delay]="'220ms'">
|
||||||
<div class="section-top-row">
|
<div class="section-top-row">
|
||||||
<div class="card-modern card-status">
|
<div class="card-modern card-status">
|
||||||
<div class="card-header-clean">
|
<div class="card-header-clean">
|
||||||
<div class="header-icon brand"><i class="bi bi-pie-chart-fill"></i></div>
|
<div class="header-icon brand"><i class="bi bi-pie-chart-fill"></i></div>
|
||||||
<div class="header-text">
|
<div class="header-text">
|
||||||
<h3>Status da Base</h3>
|
<h3>Status da Base</h3>
|
||||||
<p>Distribuição atual das linhas</p>
|
<p>Distribuição atual das linhas</p>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="card-body-split">
|
|
||||||
<div class="chart-wrapper-pie">
|
|
||||||
<canvas #chartStatusPie></canvas>
|
|
||||||
</div>
|
|
||||||
<div class="status-list">
|
|
||||||
<div class="status-item">
|
|
||||||
<span class="dot d-active"></span>
|
|
||||||
<span class="lbl">Ativas</span>
|
|
||||||
<span class="val">{{ statusResumo.ativos | number:'1.0-0' }}</span>
|
|
||||||
</div>
|
|
||||||
<div class="status-item">
|
|
||||||
<span class="dot d-reserve"></span>
|
|
||||||
<span class="lbl">Reservas</span>
|
|
||||||
<span class="val">{{ statusResumo.reservas | number:'1.0-0' }}</span>
|
|
||||||
</div>
|
|
||||||
<div class="status-item">
|
|
||||||
<span class="dot d-blocked"></span>
|
|
||||||
<span class="lbl">Bloq. (Perda/Roubo)</span>
|
|
||||||
<span class="val">{{ statusResumo.perdaRoubo | number:'1.0-0' }}</span>
|
|
||||||
</div>
|
|
||||||
<div class="status-item">
|
|
||||||
<span class="dot d-blocked-soft"></span>
|
|
||||||
<span class="lbl">Bloq. (120 dias)</span>
|
|
||||||
<span class="val">{{ statusResumo.bloq120 | number:'1.0-0' }}</span>
|
|
||||||
</div>
|
|
||||||
<div class="status-item total-row">
|
|
||||||
<span class="lbl">Total Geral</span>
|
|
||||||
<span class="val">{{ statusResumo.total | number:'1.0-0' }}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="card-body-split">
|
||||||
<div class="card-modern card-adicionais">
|
|
||||||
<div class="card-header-clean">
|
|
||||||
<div class="header-icon blue"><i class="bi bi-diagram-3-fill"></i></div>
|
|
||||||
<div class="header-text">
|
|
||||||
<h3>Serviços Adicionais</h3>
|
|
||||||
<p>Comparativo de linhas com e sem adicionais (Geral)</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="card-body-adicionais">
|
|
||||||
<div class="chart-wrapper-pie-sm">
|
|
||||||
<canvas #chartAdicionaisComparativo></canvas>
|
|
||||||
</div>
|
|
||||||
<div class="compare-list">
|
|
||||||
<div class="compare-item">
|
|
||||||
<span class="dot d-com-add"></span>
|
|
||||||
<span class="lbl">Com adicionais</span>
|
|
||||||
<span class="val">{{ adicionaisComparativo.com | number:'1.0-0' }}</span>
|
|
||||||
<span class="pct">{{ adicionaisComparativo.pctCom }}</span>
|
|
||||||
</div>
|
|
||||||
<div class="compare-item">
|
|
||||||
<span class="dot d-sem-add"></span>
|
|
||||||
<span class="lbl">Sem adicionais</span>
|
|
||||||
<span class="val">{{ adicionaisComparativo.sem | number:'1.0-0' }}</span>
|
|
||||||
<span class="pct">{{ adicionaisComparativo.pctSem }}</span>
|
|
||||||
</div>
|
|
||||||
<div class="compare-item total-row">
|
|
||||||
<span class="lbl">Total analisado</span>
|
|
||||||
<span class="val">{{ adicionaisComparativo.total | number:'1.0-0' }}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="dashboard-section fade-in-up" [style.animation-delay]="'280ms'">
|
|
||||||
<div class="grid-halves">
|
|
||||||
<div class="card-modern">
|
|
||||||
<div class="card-header-clean">
|
|
||||||
<div class="header-icon warning"><i class="bi bi-shield-exclamation"></i></div>
|
|
||||||
<div class="header-text">
|
|
||||||
<h3>Vigência (Buckets)</h3>
|
|
||||||
<p>Status de vencimento atual</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="chart-wrapper-pie">
|
<div class="chart-wrapper-pie">
|
||||||
<canvas #chartVigenciaSupervisao></canvas>
|
<canvas #chartStatusPie></canvas>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<div class="status-list">
|
||||||
|
<div class="status-item">
|
||||||
<div class="card-modern">
|
<span class="dot d-active"></span>
|
||||||
<div class="card-header-clean">
|
<span class="lbl">Ativas</span>
|
||||||
<div class="header-icon blue"><i class="bi bi-globe2"></i></div>
|
<span class="val">{{ statusResumo.ativos | number:'1.0-0' }}</span>
|
||||||
<div class="header-text">
|
</div>
|
||||||
<h3>Vivo Travel</h3>
|
<div class="status-item">
|
||||||
<p>Linhas com e sem serviço ativo</p>
|
<span class="dot d-reserve"></span>
|
||||||
|
<span class="lbl">Reservas</span>
|
||||||
|
<span class="val">{{ statusResumo.reservas | number:'1.0-0' }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="status-item">
|
||||||
|
<span class="dot d-blocked"></span>
|
||||||
|
<span class="lbl">Bloq. (Perda/Roubo)</span>
|
||||||
|
<span class="val">{{ statusResumo.perdaRoubo | number:'1.0-0' }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="status-item">
|
||||||
|
<span class="dot d-blocked-soft"></span>
|
||||||
|
<span class="lbl">Bloq. (120 dias)</span>
|
||||||
|
<span class="val">{{ statusResumo.bloq120 | number:'1.0-0' }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="status-item total-row">
|
||||||
|
<span class="lbl">Total Geral</span>
|
||||||
|
<span class="val">{{ statusResumo.total | number:'1.0-0' }}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
<div class="chart-wrapper-pie">
|
|
||||||
<canvas #chartTravelMundo></canvas>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="grid-triples mt-3">
|
<div class="card-modern card-adicionais">
|
||||||
<div class="card-modern">
|
<div class="card-header-clean">
|
||||||
<div class="card-header-clean">
|
<div class="header-icon blue"><i class="bi bi-diagram-3-fill"></i></div>
|
||||||
<div class="header-text">
|
<div class="header-text">
|
||||||
<h3>Linhas por Franquia</h3>
|
<h3>Serviços Adicionais</h3>
|
||||||
<p>Distribuição da base por faixa de franquia</p>
|
<p>Comparativo de linhas com e sem adicionais (Geral)</p>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="chart-wrapper-bar compact">
|
|
||||||
<canvas #chartLinhasPorFranquia></canvas>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="card-body-adicionais">
|
||||||
<div class="card-modern">
|
<div class="chart-wrapper-pie-sm">
|
||||||
<div class="card-header-clean">
|
<canvas #chartAdicionaisComparativo></canvas>
|
||||||
<div class="header-text">
|
</div>
|
||||||
<h3>Adicionais Pagos (Serviços)</h3>
|
<div class="compare-list">
|
||||||
<p>Quantidade de linhas por serviço adicional ativo</p>
|
<div class="compare-item">
|
||||||
|
<span class="dot d-com-add"></span>
|
||||||
|
<span class="lbl">Com adicionais</span>
|
||||||
|
<span class="val">{{ adicionaisComparativo.com | number:'1.0-0' }}</span>
|
||||||
|
<span class="pct">{{ adicionaisComparativo.pctCom }}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<div class="compare-item">
|
||||||
<div class="chart-wrapper-bar compact">
|
<span class="dot d-sem-add"></span>
|
||||||
<canvas #chartAdicionaisPagos></canvas>
|
<span class="lbl">Sem adicionais</span>
|
||||||
</div>
|
<span class="val">{{ adicionaisComparativo.sem | number:'1.0-0' }}</span>
|
||||||
</div>
|
<span class="pct">{{ adicionaisComparativo.pctSem }}</span>
|
||||||
|
</div>
|
||||||
<div class="card-modern">
|
<div class="compare-item total-row">
|
||||||
<div class="card-header-clean">
|
<span class="lbl">Total analisado</span>
|
||||||
<div class="header-text">
|
<span class="val">{{ adicionaisComparativo.total | number:'1.0-0' }}</span>
|
||||||
<h3>Tipo de Chip</h3>
|
|
||||||
<p>Quantidade de linhas e-SIM e SIMCARD</p>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
<div class="chart-wrapper-pie">
|
|
||||||
<canvas #chartTipoChip></canvas>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="context-title fade-in-up" [style.animation-delay]="'320ms'">
|
<div class="dashboard-section fade-in-up" [style.animation-delay]="'280ms'">
|
||||||
<h2>Página Resumo</h2>
|
<div class="grid-halves">
|
||||||
<p>Indicadores do Resumo focados em quantidade e distribuição de linhas.</p>
|
<div class="card-modern">
|
||||||
|
<div class="card-header-clean">
|
||||||
|
<div class="header-icon warning"><i class="bi bi-shield-exclamation"></i></div>
|
||||||
|
<div class="header-text">
|
||||||
|
<h3>Vigência (Buckets)</h3>
|
||||||
|
<p>Status de vencimento atual</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="chart-wrapper-pie">
|
||||||
|
<canvas #chartVigenciaSupervisao></canvas>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card-modern">
|
||||||
|
<div class="card-header-clean">
|
||||||
|
<div class="header-icon blue"><i class="bi bi-globe2"></i></div>
|
||||||
|
<div class="header-text">
|
||||||
|
<h3>Vivo Travel</h3>
|
||||||
|
<p>Linhas com e sem serviço ativo</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="chart-wrapper-pie">
|
||||||
|
<canvas #chartTravelMundo></canvas>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="dashboard-section fade-in-up" [style.animation-delay]="'360ms'">
|
<div class="grid-triples mt-3">
|
||||||
<div class="card-modern full-width">
|
<div class="card-modern">
|
||||||
<div class="toolbar-header">
|
<div class="card-header-clean">
|
||||||
<div class="title-group">
|
<div class="header-text">
|
||||||
<i class="bi bi-bar-chart-line text-brand"></i>
|
<h3>Linhas por Franquia</h3>
|
||||||
<div>
|
<p>Distribuição da base por faixa de franquia</p>
|
||||||
<h3>Resumo Operacional de Linhas</h3>
|
|
||||||
<p>Dados consolidados da página Resumo sem foco financeiro</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="chart-wrapper-bar compact">
|
||||||
|
<canvas #chartLinhasPorFranquia></canvas>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="toolbar-controls">
|
<div class="card-modern">
|
||||||
<div class="control-group">
|
<div class="card-header-clean">
|
||||||
<label>Visualizar Top</label>
|
<div class="header-text">
|
||||||
<select
|
<h3>Adicionais Pagos (Serviços)</h3>
|
||||||
[value]="resumoTopN"
|
<p>Quantidade de linhas por serviço adicional ativo</p>
|
||||||
(change)="resumoTopN = +($any($event.target).value); onResumoTopNChange()"
|
</div>
|
||||||
class="form-select-sm">
|
</div>
|
||||||
<option *ngFor="let size of resumoTopOptions" [value]="size">{{ size }}</option>
|
<div class="chart-wrapper-bar compact">
|
||||||
</select>
|
<canvas #chartAdicionaisPagos></canvas>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="divider-v"></div>
|
<div class="card-modern">
|
||||||
|
<div class="card-header-clean">
|
||||||
|
<div class="header-text">
|
||||||
|
<h3>Tipo de Chip</h3>
|
||||||
|
<p>Quantidade de linhas e-SIM e SIMCARD</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="chart-wrapper-pie">
|
||||||
|
<canvas #chartTipoChip></canvas>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<a class="btn-link" [routerLink]="['/resumo']" [queryParams]="{ tab: 'totais' }">Ver Página Resumo <i class="bi bi-arrow-right"></i></a>
|
<div class="context-title fade-in-up" [style.animation-delay]="'320ms'">
|
||||||
|
<h2>Página Resumo</h2>
|
||||||
|
<p>Indicadores do Resumo focados em quantidade e distribuição de linhas.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="dashboard-section fade-in-up" [style.animation-delay]="'360ms'">
|
||||||
|
<div class="card-modern full-width">
|
||||||
|
<div class="toolbar-header">
|
||||||
|
<div class="title-group">
|
||||||
|
<i class="bi bi-bar-chart-line text-brand"></i>
|
||||||
|
<div>
|
||||||
|
<h3>Resumo Operacional de Linhas</h3>
|
||||||
|
<p>Dados consolidados da página Resumo sem foco financeiro</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="card-body-grid">
|
<div class="toolbar-controls">
|
||||||
<div class="loading-overlay" *ngIf="resumoLoading">
|
<div class="control-group">
|
||||||
<div class="spinner-border text-brand" role="status"></div>
|
<label>Visualizar Top</label>
|
||||||
|
<select
|
||||||
|
[value]="resumoTopN"
|
||||||
|
(change)="resumoTopN = +($any($event.target).value); onResumoTopNChange()"
|
||||||
|
class="form-select-sm">
|
||||||
|
<option *ngFor="let size of resumoTopOptions" [value]="size">{{ size }}</option>
|
||||||
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="error-state" *ngIf="!resumoLoading && resumoError">
|
<div class="divider-v"></div>
|
||||||
<i class="bi bi-exclamation-circle"></i>
|
|
||||||
<span>{{ resumoError }}</span>
|
<a class="btn-link" [routerLink]="['/resumo']" [queryParams]="{ tab: 'totais' }">Ver Página Resumo <i class="bi bi-arrow-right"></i></a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card-body-grid">
|
||||||
|
<div class="loading-overlay" *ngIf="resumoLoading">
|
||||||
|
<div class="spinner-border text-brand" role="status"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="error-state" *ngIf="!resumoLoading && resumoError">
|
||||||
|
<i class="bi bi-exclamation-circle"></i>
|
||||||
|
<span>{{ resumoError }}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="analytics-grid" *ngIf="!resumoLoading && !resumoError">
|
||||||
|
<div class="mini-chart-card">
|
||||||
|
<h6>Top Clientes (Qtd. Linhas)</h6>
|
||||||
|
<div class="chart-area"><canvas #chartResumoTopClientes></canvas></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="analytics-grid" *ngIf="!resumoLoading && !resumoError">
|
<div class="mini-chart-card">
|
||||||
<div class="mini-chart-card">
|
<h6>Top Planos (Qtd. Linhas)</h6>
|
||||||
<h6>Top Clientes (Qtd. Linhas)</h6>
|
<div class="chart-area"><canvas #chartResumoTopPlanos></canvas></div>
|
||||||
<div class="chart-area"><canvas #chartResumoTopClientes></canvas></div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="mini-chart-card">
|
<div class="mini-chart-card">
|
||||||
<h6>Top Planos (Qtd. Linhas)</h6>
|
<h6>PF vs PJ (Qtd. Linhas)</h6>
|
||||||
<div class="chart-area"><canvas #chartResumoTopPlanos></canvas></div>
|
<div class="chart-area"><canvas #chartResumoPfPjLinhas></canvas></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mini-chart-card">
|
<div class="mini-chart-card">
|
||||||
<h6>PF vs PJ (Qtd. Linhas)</h6>
|
<h6>Reserva por DDD</h6>
|
||||||
<div class="chart-area"><canvas #chartResumoPfPjLinhas></canvas></div>
|
<div class="chart-area"><canvas #chartResumoReservaDdd></canvas></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mini-chart-card">
|
<div class="mini-chart-card mini-metric-card">
|
||||||
<h6>Reserva por DDD</h6>
|
<h6>DIFERENÇA PJ X PF</h6>
|
||||||
<div class="chart-area"><canvas #chartResumoReservaDdd></canvas></div>
|
<div class="metric-stack">
|
||||||
</div>
|
<div class="metric-line">
|
||||||
|
<span>PF (Linhas)</span>
|
||||||
<div class="mini-chart-card mini-metric-card">
|
<strong>{{ formatInt(resumoDiferencaPjPf.pfLinhas) }}</strong>
|
||||||
<h6>DIFERENÇA PJ X PF</h6>
|
</div>
|
||||||
<div class="metric-stack">
|
<div class="metric-line">
|
||||||
<div class="metric-line">
|
<span>PJ (Linhas)</span>
|
||||||
<span>PF (Linhas)</span>
|
<strong>{{ formatInt(resumoDiferencaPjPf.pjLinhas) }}</strong>
|
||||||
<strong>{{ formatInt(resumoDiferencaPjPf.pfLinhas) }}</strong>
|
</div>
|
||||||
</div>
|
<div class="metric-line">
|
||||||
<div class="metric-line">
|
<span>Total Linhas</span>
|
||||||
<span>PJ (Linhas)</span>
|
<strong>{{ formatInt(resumoDiferencaPjPf.totalLinhas) }}</strong>
|
||||||
<strong>{{ formatInt(resumoDiferencaPjPf.pjLinhas) }}</strong>
|
|
||||||
</div>
|
|
||||||
<div class="metric-line">
|
|
||||||
<span>Total Linhas</span>
|
|
||||||
<strong>{{ formatInt(resumoDiferencaPjPf.totalLinhas) }}</strong>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="context-title fade-in-up" [style.animation-delay]="'420ms'">
|
<div class="context-title fade-in-up" [style.animation-delay]="'420ms'">
|
||||||
<h2>Histórico</h2>
|
<h2>Histórico</h2>
|
||||||
<p>Séries mensais para acompanhamento contínuo de movimentações e vigência.</p>
|
<p>Séries mensais para acompanhamento contínuo de movimentações e vigência.</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="dashboard-section fade-in-up" [style.animation-delay]="'460ms'">
|
<div class="dashboard-section fade-in-up" [style.animation-delay]="'460ms'">
|
||||||
<div class="history-grid">
|
<div class="history-grid">
|
||||||
<div class="card-modern">
|
<div class="card-modern">
|
||||||
<div class="card-header-clean">
|
<div class="card-header-clean">
|
||||||
<div class="header-text">
|
<div class="header-text">
|
||||||
<h3>MUREG (12 Meses)</h3>
|
<h3>MUREG (12 Meses)</h3>
|
||||||
<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 class="chart-wrapper-bar compact-half">
|
|
||||||
<canvas #chartMureg12></canvas>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="chart-wrapper-bar compact-half">
|
||||||
|
<canvas #chartMureg12></canvas>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="card-modern">
|
<div class="card-modern">
|
||||||
<div class="card-header-clean">
|
<div class="card-header-clean">
|
||||||
<div class="header-text">
|
<div class="header-text">
|
||||||
<h3>Troca de Número (12 Meses)</h3>
|
<h3>Troca de Número (12 Meses)</h3>
|
||||||
<p>Histórico mensal de trocas realizadas</p>
|
<p>Histórico mensal de trocas realizadas</p>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="chart-wrapper-bar compact-half">
|
|
||||||
<canvas #chartTroca12></canvas>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="chart-wrapper-bar compact-half">
|
||||||
|
<canvas #chartTroca12></canvas>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="card-modern">
|
<div class="card-modern">
|
||||||
<div class="card-header-clean">
|
<div class="card-header-clean">
|
||||||
<div class="header-icon purple"><i class="bi bi-calendar2-check"></i></div>
|
<div class="header-icon purple"><i class="bi bi-calendar2-check"></i></div>
|
||||||
<div class="header-text">
|
<div class="header-text">
|
||||||
<h3>Vigência (Próx. 12 Meses)</h3>
|
<h3>Vigência (Próx. 12 Meses)</h3>
|
||||||
<p>Contratos a encerrar por mês</p>
|
<p>Contratos a encerrar por mês</p>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="chart-wrapper-bar compact-half">
|
|
||||||
<canvas #chartVigenciaMesAno></canvas>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="chart-wrapper-bar compact-half">
|
||||||
|
<canvas #chartVigenciaMesAno></canvas>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</ng-container>
|
</div>
|
||||||
|
|
||||||
<ng-template #clienteDashboard>
|
|
||||||
<div class="dashboard-section fade-in-up" [style.animation-delay]="'180ms'">
|
|
||||||
<div class="section-top-row">
|
|
||||||
<div class="card-modern card-status">
|
|
||||||
<div class="card-header-clean">
|
|
||||||
<div class="header-icon brand"><i class="bi bi-pie-chart-fill"></i></div>
|
|
||||||
<div class="header-text">
|
|
||||||
<h3>Status da Base</h3>
|
|
||||||
<p>Distribuição atual das linhas</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="card-body-split">
|
|
||||||
<div class="chart-wrapper-pie">
|
|
||||||
<canvas #chartStatusPie></canvas>
|
|
||||||
</div>
|
|
||||||
<div class="status-list">
|
|
||||||
<div class="status-item">
|
|
||||||
<span class="dot d-active"></span>
|
|
||||||
<span class="lbl">Ativas</span>
|
|
||||||
<span class="val">{{ statusResumo.ativos | number:'1.0-0' }}</span>
|
|
||||||
</div>
|
|
||||||
<div class="status-item">
|
|
||||||
<span class="dot d-blocked"></span>
|
|
||||||
<span class="lbl">Bloqueadas</span>
|
|
||||||
<span class="val">{{ statusResumo.bloqueadas | number:'1.0-0' }}</span>
|
|
||||||
</div>
|
|
||||||
<div class="status-item">
|
|
||||||
<span class="dot d-reserve"></span>
|
|
||||||
<span class="lbl">Reserva</span>
|
|
||||||
<span class="val">{{ statusResumo.reservas | number:'1.0-0' }}</span>
|
|
||||||
</div>
|
|
||||||
<div class="status-item total-row">
|
|
||||||
<span class="lbl">Total</span>
|
|
||||||
<span class="val">{{ statusResumo.total | number:'1.0-0' }}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</ng-template>
|
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
|
||||||
|
|
@ -20,7 +20,6 @@ import {
|
||||||
ResumoResponse,
|
ResumoResponse,
|
||||||
LineTotal,
|
LineTotal,
|
||||||
} from '../../services/resumo.service';
|
} from '../../services/resumo.service';
|
||||||
import { AuthService } from '../../services/auth.service';
|
|
||||||
|
|
||||||
// --- Interfaces (Mantidas intactas para não quebrar contrato) ---
|
// --- Interfaces (Mantidas intactas para não quebrar contrato) ---
|
||||||
type KpiCard = {
|
type KpiCard = {
|
||||||
|
|
@ -219,7 +218,6 @@ export class Dashboard implements OnInit, AfterViewInit, OnDestroy {
|
||||||
|
|
||||||
loading = true;
|
loading = true;
|
||||||
errorMsg: string | null = null;
|
errorMsg: string | null = null;
|
||||||
isCliente = false;
|
|
||||||
|
|
||||||
kpis: KpiCard[] = [];
|
kpis: KpiCard[] = [];
|
||||||
|
|
||||||
|
|
@ -242,7 +240,6 @@ export class Dashboard implements OnInit, AfterViewInit, OnDestroy {
|
||||||
statusResumo = {
|
statusResumo = {
|
||||||
total: 0,
|
total: 0,
|
||||||
ativos: 0,
|
ativos: 0,
|
||||||
bloqueadas: 0,
|
|
||||||
perdaRoubo: 0,
|
perdaRoubo: 0,
|
||||||
bloq120: 0,
|
bloq120: 0,
|
||||||
reservas: 0,
|
reservas: 0,
|
||||||
|
|
@ -334,7 +331,6 @@ export class Dashboard implements OnInit, AfterViewInit, OnDestroy {
|
||||||
constructor(
|
constructor(
|
||||||
private http: HttpClient,
|
private http: HttpClient,
|
||||||
private resumoService: ResumoService,
|
private resumoService: ResumoService,
|
||||||
private authService: AuthService,
|
|
||||||
@Inject(PLATFORM_ID) private platformId: object
|
@Inject(PLATFORM_ID) private platformId: object
|
||||||
) {
|
) {
|
||||||
const raw = (environment.apiUrl || '').replace(/\/+$/, '');
|
const raw = (environment.apiUrl || '').replace(/\/+$/, '');
|
||||||
|
|
@ -344,15 +340,9 @@ export class Dashboard implements OnInit, AfterViewInit, OnDestroy {
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
if (!isPlatformBrowser(this.platformId)) return;
|
if (!isPlatformBrowser(this.platformId)) return;
|
||||||
|
|
||||||
const isSysAdmin = this.authService.hasRole('sysadmin');
|
|
||||||
const isGestor = this.authService.hasRole('gestor');
|
|
||||||
this.isCliente = !(isSysAdmin || isGestor);
|
|
||||||
|
|
||||||
this.loadDashboard();
|
this.loadDashboard();
|
||||||
if (!this.isCliente) {
|
this.loadInsights();
|
||||||
this.loadInsights();
|
this.loadResumoExecutive();
|
||||||
this.loadResumoExecutive();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ngAfterViewInit(): void {
|
ngAfterViewInit(): void {
|
||||||
|
|
@ -476,7 +466,6 @@ export class Dashboard implements OnInit, AfterViewInit, OnDestroy {
|
||||||
this.statusResumo = {
|
this.statusResumo = {
|
||||||
total: k.totalLinhas ?? 0,
|
total: k.totalLinhas ?? 0,
|
||||||
ativos: k.ativos ?? 0,
|
ativos: k.ativos ?? 0,
|
||||||
bloqueadas: k.bloqueados ?? 0,
|
|
||||||
perdaRoubo: k.bloqueadosPerdaRoubo ?? 0,
|
perdaRoubo: k.bloqueadosPerdaRoubo ?? 0,
|
||||||
bloq120: k.bloqueados120Dias ?? 0,
|
bloq120: k.bloqueados120Dias ?? 0,
|
||||||
reservas: k.reservas ?? 0,
|
reservas: k.reservas ?? 0,
|
||||||
|
|
@ -923,40 +912,6 @@ export class Dashboard implements OnInit, AfterViewInit, OnDestroy {
|
||||||
}
|
}
|
||||||
|
|
||||||
private rebuildPrimaryKpis() {
|
private rebuildPrimaryKpis() {
|
||||||
if (this.isCliente) {
|
|
||||||
this.kpis = [
|
|
||||||
{
|
|
||||||
key: 'linhas_total',
|
|
||||||
title: 'Total de Linhas',
|
|
||||||
value: this.formatInt(this.dashboardRaw?.totalLinhas ?? this.statusResumo.total),
|
|
||||||
icon: 'bi bi-sim-fill',
|
|
||||||
hint: 'Base geral',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'linhas_ativas',
|
|
||||||
title: 'Linhas Ativas',
|
|
||||||
value: this.formatInt(this.dashboardRaw?.ativos ?? this.statusResumo.ativos),
|
|
||||||
icon: 'bi bi-check2-circle',
|
|
||||||
hint: 'Status ativo',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'linhas_bloqueadas',
|
|
||||||
title: 'Linhas Bloqueadas',
|
|
||||||
value: this.formatInt(this.dashboardRaw?.bloqueados ?? this.statusResumo.bloqueadas),
|
|
||||||
icon: 'bi bi-slash-circle',
|
|
||||||
hint: 'Todos os bloqueios',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'linhas_reserva',
|
|
||||||
title: 'Linhas em Reserva',
|
|
||||||
value: this.formatInt(this.dashboardRaw?.reservas ?? this.statusResumo.reservas),
|
|
||||||
icon: 'bi bi-inboxes-fill',
|
|
||||||
hint: 'Base de reserva',
|
|
||||||
},
|
|
||||||
];
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const cards: KpiCard[] = [];
|
const cards: KpiCard[] = [];
|
||||||
const used = new Set<string>();
|
const used = new Set<string>();
|
||||||
const add = (key: string, title: string, value: string, icon: string, hint?: string) => {
|
const add = (key: string, title: string, value: string, icon: string, hint?: string) => {
|
||||||
|
|
@ -1014,22 +969,18 @@ export class Dashboard implements OnInit, AfterViewInit, OnDestroy {
|
||||||
if (!this.viewReady || !this.dataReady) return;
|
if (!this.viewReady || !this.dataReady) return;
|
||||||
|
|
||||||
requestAnimationFrame(() => {
|
requestAnimationFrame(() => {
|
||||||
const canvases = (
|
const canvases = [
|
||||||
this.isCliente
|
this.chartStatusPie?.nativeElement,
|
||||||
? [this.chartStatusPie?.nativeElement]
|
this.chartAdicionaisComparativo?.nativeElement,
|
||||||
: [
|
this.chartVigenciaMesAno?.nativeElement,
|
||||||
this.chartStatusPie?.nativeElement,
|
this.chartVigenciaSupervisao?.nativeElement,
|
||||||
this.chartAdicionaisComparativo?.nativeElement,
|
this.chartMureg12?.nativeElement,
|
||||||
this.chartVigenciaMesAno?.nativeElement,
|
this.chartTroca12?.nativeElement,
|
||||||
this.chartVigenciaSupervisao?.nativeElement,
|
this.chartLinhasPorFranquia?.nativeElement,
|
||||||
this.chartMureg12?.nativeElement,
|
this.chartAdicionaisPagos?.nativeElement,
|
||||||
this.chartTroca12?.nativeElement,
|
this.chartTipoChip?.nativeElement,
|
||||||
this.chartLinhasPorFranquia?.nativeElement,
|
this.chartTravelMundo?.nativeElement,
|
||||||
this.chartAdicionaisPagos?.nativeElement,
|
].filter(Boolean) as HTMLCanvasElement[];
|
||||||
this.chartTipoChip?.nativeElement,
|
|
||||||
this.chartTravelMundo?.nativeElement,
|
|
||||||
]
|
|
||||||
).filter(Boolean) as HTMLCanvasElement[];
|
|
||||||
|
|
||||||
if (!canvases.length) return;
|
if (!canvases.length) return;
|
||||||
if (canvases.some((c) => c.clientWidth === 0 || c.clientHeight === 0)) {
|
if (canvases.some((c) => c.clientWidth === 0 || c.clientHeight === 0)) {
|
||||||
|
|
@ -1082,36 +1033,26 @@ export class Dashboard implements OnInit, AfterViewInit, OnDestroy {
|
||||||
|
|
||||||
// 1. Status Pie
|
// 1. Status Pie
|
||||||
if (this.chartStatusPie?.nativeElement) {
|
if (this.chartStatusPie?.nativeElement) {
|
||||||
const chartLabels = this.isCliente
|
|
||||||
? ['Ativas', 'Bloqueadas', 'Reservas']
|
|
||||||
: ['Ativos', 'Perda/Roubo', 'Bloq 120d', 'Reservas', 'Outros'];
|
|
||||||
const chartData = this.isCliente
|
|
||||||
? [this.statusResumo.ativos, this.statusResumo.bloqueadas, this.statusResumo.reservas]
|
|
||||||
: [
|
|
||||||
this.statusResumo.ativos,
|
|
||||||
this.statusResumo.perdaRoubo,
|
|
||||||
this.statusResumo.bloq120,
|
|
||||||
this.statusResumo.reservas,
|
|
||||||
this.statusResumo.outras,
|
|
||||||
];
|
|
||||||
const chartColors = this.isCliente
|
|
||||||
? [palette.status.ativos, palette.status.blocked, palette.status.reserve]
|
|
||||||
: [
|
|
||||||
palette.status.ativos,
|
|
||||||
palette.status.blocked,
|
|
||||||
palette.status.purple,
|
|
||||||
palette.status.reserve,
|
|
||||||
'#cbd5e1',
|
|
||||||
];
|
|
||||||
|
|
||||||
this.chartPie = new Chart(this.chartStatusPie.nativeElement, {
|
this.chartPie = new Chart(this.chartStatusPie.nativeElement, {
|
||||||
type: 'doughnut',
|
type: 'doughnut',
|
||||||
data: {
|
data: {
|
||||||
labels: chartLabels,
|
labels: ['Ativos', 'Perda/Roubo', 'Bloq 120d', 'Reservas', 'Outros'],
|
||||||
datasets: [{
|
datasets: [{
|
||||||
data: chartData,
|
data: [
|
||||||
|
this.statusResumo.ativos,
|
||||||
|
this.statusResumo.perdaRoubo,
|
||||||
|
this.statusResumo.bloq120,
|
||||||
|
this.statusResumo.reservas,
|
||||||
|
this.statusResumo.outras
|
||||||
|
],
|
||||||
borderWidth: 0,
|
borderWidth: 0,
|
||||||
backgroundColor: chartColors,
|
backgroundColor: [
|
||||||
|
palette.status.ativos,
|
||||||
|
palette.status.blocked,
|
||||||
|
palette.status.purple,
|
||||||
|
palette.status.reserve,
|
||||||
|
'#cbd5e1'
|
||||||
|
],
|
||||||
hoverOffset: 4
|
hoverOffset: 4
|
||||||
}]
|
}]
|
||||||
},
|
},
|
||||||
|
|
@ -1124,10 +1065,6 @@ export class Dashboard implements OnInit, AfterViewInit, OnDestroy {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.isCliente) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.chartAdicionaisComparativo?.nativeElement) {
|
if (this.chartAdicionaisComparativo?.nativeElement) {
|
||||||
this.chartAdicionaisComparativoDoughnut = new Chart(this.chartAdicionaisComparativo.nativeElement, {
|
this.chartAdicionaisComparativoDoughnut = new Chart(this.chartAdicionaisComparativo.nativeElement, {
|
||||||
type: 'doughnut',
|
type: 'doughnut',
|
||||||
|
|
|
||||||
|
|
@ -160,7 +160,7 @@ export class Faturamento implements AfterViewInit, OnDestroy {
|
||||||
if (!isPlatformBrowser(this.platformId)) return;
|
if (!isPlatformBrowser(this.platformId)) return;
|
||||||
|
|
||||||
this.initAnimations();
|
this.initAnimations();
|
||||||
this.isAdmin = this.authService.hasRole('sysadmin');
|
this.isAdmin = this.authService.hasRole('admin');
|
||||||
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
this.refreshData(true);
|
this.refreshData(true);
|
||||||
|
|
|
||||||
|
|
@ -46,7 +46,6 @@
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="btn btn-brand btn-sm"
|
class="btn btn-brand btn-sm"
|
||||||
*ngIf="!isClientRestricted"
|
|
||||||
(click)="onCadastrarLinha()"
|
(click)="onCadastrarLinha()"
|
||||||
[disabled]="loading">
|
[disabled]="loading">
|
||||||
<i class="bi bi-plus-circle me-1"></i> Novo Cliente
|
<i class="bi bi-plus-circle me-1"></i> Novo Cliente
|
||||||
|
|
@ -59,21 +58,19 @@
|
||||||
<button type="button" class="filter-tab" [class.active]="filterSkil === 'ALL'" (click)="setFilter('ALL')" [disabled]="loading">
|
<button type="button" class="filter-tab" [class.active]="filterSkil === 'ALL'" (click)="setFilter('ALL')" [disabled]="loading">
|
||||||
Todos
|
Todos
|
||||||
</button>
|
</button>
|
||||||
<ng-container *ngIf="!isClientRestricted">
|
<button type="button" class="filter-tab" [class.active]="filterSkil === 'PF'" (click)="setFilter('PF')" [disabled]="loading">
|
||||||
<button type="button" class="filter-tab" [class.active]="filterSkil === 'PF'" (click)="setFilter('PF')" [disabled]="loading">
|
<i class="bi bi-person me-1"></i> Pessoa Física
|
||||||
<i class="bi bi-person me-1"></i> Pessoa Física
|
</button>
|
||||||
</button>
|
<button type="button" class="filter-tab" [class.active]="filterSkil === 'PJ'" (click)="setFilter('PJ')" [disabled]="loading">
|
||||||
<button type="button" class="filter-tab" [class.active]="filterSkil === 'PJ'" (click)="setFilter('PJ')" [disabled]="loading">
|
<i class="bi bi-building me-1"></i> Pessoa Jurídica
|
||||||
<i class="bi bi-building me-1"></i> Pessoa Jurídica
|
</button>
|
||||||
</button>
|
<button type="button" class="filter-tab" [class.active]="filterSkil === 'RESERVA'" (click)="setFilter('RESERVA')" [disabled]="loading">
|
||||||
<button type="button" class="filter-tab" [class.active]="filterSkil === 'RESERVA'" (click)="setFilter('RESERVA')" [disabled]="loading">
|
<i class="bi bi-archive me-1"></i> Reservas
|
||||||
<i class="bi bi-archive me-1"></i> Reservas
|
</button>
|
||||||
</button>
|
|
||||||
</ng-container>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- CLIENTE MULTI-SELECT -->
|
<!-- CLIENTE MULTI-SELECT -->
|
||||||
<div class="client-filter-wrap" *ngIf="!isClientRestricted" (click)="$event.stopPropagation()">
|
<div class="client-filter-wrap" (click)="$event.stopPropagation()">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="btn-client-filter"
|
class="btn-client-filter"
|
||||||
|
|
@ -126,7 +123,7 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="additional-filter-wrap" *ngIf="!isClientRestricted" (click)="$event.stopPropagation()">
|
<div class="additional-filter-wrap" (click)="$event.stopPropagation()">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="btn-client-filter btn-additional-filter"
|
class="btn-client-filter btn-additional-filter"
|
||||||
|
|
@ -214,7 +211,7 @@
|
||||||
|
|
||||||
<!-- KPIs -->
|
<!-- KPIs -->
|
||||||
<div class="geral-kpis mt-4 animate-fade-in" *ngIf="isGroupMode">
|
<div class="geral-kpis mt-4 animate-fade-in" *ngIf="isGroupMode">
|
||||||
<div class="kpi" *ngIf="!isClientRestricted">
|
<div class="kpi">
|
||||||
<span class="lbl">Total Clientes</span>
|
<span class="lbl">Total Clientes</span>
|
||||||
<span class="val val-loading" *ngIf="isKpiLoading">
|
<span class="val val-loading" *ngIf="isKpiLoading">
|
||||||
<span class="spinner-border spinner-border-sm text-brand"></span>
|
<span class="spinner-border spinner-border-sm text-brand"></span>
|
||||||
|
|
@ -305,7 +302,7 @@
|
||||||
<div class="group-body" *ngIf="expandedGroup === group.cliente">
|
<div class="group-body" *ngIf="expandedGroup === group.cliente">
|
||||||
<div class="d-flex justify-content-between align-items-center px-4 py-2 border-bottom bg-white">
|
<div class="d-flex justify-content-between align-items-center px-4 py-2 border-bottom bg-white">
|
||||||
<small class="text-muted fw-bold">Gerenciar Grupo</small>
|
<small class="text-muted fw-bold">Gerenciar Grupo</small>
|
||||||
<button class="btn btn-sm btn-add-line-group" *ngIf="!isClientRestricted" (click)="onAddLineToGroup(group.cliente)">
|
<button class="btn btn-sm btn-add-line-group" (click)="onAddLineToGroup(group.cliente)">
|
||||||
<i class="bi bi-plus-lg me-1"></i> Adicionar Linha
|
<i class="bi bi-plus-lg me-1"></i> Adicionar Linha
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -323,7 +320,7 @@
|
||||||
<th>LINHA</th>
|
<th>LINHA</th>
|
||||||
<th>USUÁRIO</th>
|
<th>USUÁRIO</th>
|
||||||
<th>STATUS</th>
|
<th>STATUS</th>
|
||||||
<th *ngIf="!isClientRestricted">VENCIMENTO</th>
|
<th>VENCIMENTO</th>
|
||||||
<th>AÇÕES</th>
|
<th>AÇÕES</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
|
|
@ -336,15 +333,13 @@
|
||||||
<td>
|
<td>
|
||||||
<span class="status-pill" [ngClass]="statusClass(r.status)">{{ statusLabel(r.status) }}</span>
|
<span class="status-pill" [ngClass]="statusClass(r.status)">{{ statusLabel(r.status) }}</span>
|
||||||
</td>
|
</td>
|
||||||
<td class="text-muted small fw-bold" *ngIf="!isClientRestricted">{{ r.contrato }}</td>
|
<td class="text-muted small fw-bold">{{ r.contrato }}</td>
|
||||||
<td>
|
<td>
|
||||||
<div class="action-group justify-content-center">
|
<div class="action-group justify-content-center">
|
||||||
<button class="btn-icon" (click)="onDetalhes(r)" title="Detalhes"><i class="bi bi-eye"></i></button>
|
<button class="btn-icon" (click)="onDetalhes(r)" title="Detalhes"><i class="bi bi-eye"></i></button>
|
||||||
<ng-container *ngIf="!isClientRestricted">
|
<button class="btn-icon success" (click)="onFinanceiro(r)" title="Financeiro"><i class="bi bi-cash-coin"></i></button>
|
||||||
<button class="btn-icon success" (click)="onFinanceiro(r)" title="Financeiro"><i class="bi bi-cash-coin"></i></button>
|
<button class="btn-icon primary" (click)="onEditar(r)" title="Editar"><i class="bi bi-pencil-square"></i></button>
|
||||||
<button class="btn-icon primary" (click)="onEditar(r)" title="Editar"><i class="bi bi-pencil-square"></i></button>
|
<button *ngIf="isAdmin" class="btn-icon danger" (click)="onRemover(r, true)" title="Remover"><i class="bi bi-trash"></i></button>
|
||||||
<button *ngIf="isAdmin" class="btn-icon danger" (click)="onRemover(r, true)" title="Remover"><i class="bi bi-trash"></i></button>
|
|
||||||
</ng-container>
|
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|
@ -370,7 +365,7 @@
|
||||||
<col style="width: 280px;">
|
<col style="width: 280px;">
|
||||||
<col style="width: 160px;">
|
<col style="width: 160px;">
|
||||||
<col style="width: 130px;">
|
<col style="width: 130px;">
|
||||||
<col style="width: 130px;" *ngIf="!isClientRestricted">
|
<col style="width: 130px;">
|
||||||
<col style="width: 160px;">
|
<col style="width: 160px;">
|
||||||
</colgroup>
|
</colgroup>
|
||||||
|
|
||||||
|
|
@ -421,7 +416,7 @@
|
||||||
</div>
|
</div>
|
||||||
</th>
|
</th>
|
||||||
|
|
||||||
<th *ngIf="!isClientRestricted" class="sortable text-center" (click)="setSort('contrato')">
|
<th class="sortable text-center" (click)="setSort('contrato')">
|
||||||
<div class="th-content justify-content-center">
|
<div class="th-content justify-content-center">
|
||||||
VENCIMENTO
|
VENCIMENTO
|
||||||
<span class="sort-caret" [class.active]="sortKey==='contrato'">
|
<span class="sort-caret" [class.active]="sortKey==='contrato'">
|
||||||
|
|
@ -436,13 +431,13 @@
|
||||||
|
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr *ngIf="loading">
|
<tr *ngIf="loading">
|
||||||
<td [attr.colspan]="isClientRestricted ? 6 : 7" class="text-center py-5 empty-state">
|
<td colspan="7" class="text-center py-5 empty-state">
|
||||||
<span class="spinner-border spinner-border-sm me-2 text-brand"></span> Carregando...
|
<span class="spinner-border spinner-border-sm me-2 text-brand"></span> Carregando...
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|
||||||
<tr *ngIf="!loading && pagedRows.length === 0">
|
<tr *ngIf="!loading && pagedRows.length === 0">
|
||||||
<td [attr.colspan]="isClientRestricted ? 6 : 7" class="text-center py-5 empty-state text-muted fw-bold">
|
<td colspan="7" class="text-center py-5 empty-state text-muted fw-bold">
|
||||||
Nenhum registro encontrado.
|
Nenhum registro encontrado.
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|
@ -455,15 +450,13 @@
|
||||||
<span class="status-pill" [ngClass]="statusClass(r.status)" [title]="r.status || ''">{{ statusLabel(r.status) }}</span>
|
<span class="status-pill" [ngClass]="statusClass(r.status)" [title]="r.status || ''">{{ statusLabel(r.status) }}</span>
|
||||||
</td>
|
</td>
|
||||||
<td class="text-center fw-bold text-muted small">{{ r.skil }}</td>
|
<td class="text-center fw-bold text-muted small">{{ r.skil }}</td>
|
||||||
<td class="text-center fw-bold text-muted small" *ngIf="!isClientRestricted">{{ r.contrato }}</td>
|
<td class="text-center fw-bold text-muted small">{{ r.contrato }}</td>
|
||||||
<td class="text-center">
|
<td class="text-center">
|
||||||
<div class="action-group justify-content-center">
|
<div class="action-group justify-content-center">
|
||||||
<button class="btn-icon" (click)="onDetalhes(r)" title="Detalhes"><i class="bi bi-eye"></i></button>
|
<button class="btn-icon" (click)="onDetalhes(r)" title="Detalhes"><i class="bi bi-eye"></i></button>
|
||||||
<ng-container *ngIf="!isClientRestricted">
|
<button class="btn-icon success" (click)="onFinanceiro(r)" title="Financeiro"><i class="bi bi-cash-coin"></i></button>
|
||||||
<button class="btn-icon success" (click)="onFinanceiro(r)" title="Financeiro"><i class="bi bi-cash-coin"></i></button>
|
<button class="btn-icon primary" (click)="onEditar(r)" title="Editar"><i class="bi bi-pencil-square"></i></button>
|
||||||
<button class="btn-icon primary" (click)="onEditar(r)" title="Editar"><i class="bi bi-pencil-square"></i></button>
|
<button *ngIf="isAdmin" class="btn-icon danger" (click)="onRemover(r)" title="Remover"><i class="bi bi-trash"></i></button>
|
||||||
<button *ngIf="isAdmin" class="btn-icon danger" (click)="onRemover(r)" title="Remover"><i class="bi bi-trash"></i></button>
|
|
||||||
</ng-container>
|
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|
@ -1585,7 +1578,7 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="dashboard-column d-flex flex-column gap-2" *ngIf="!isClientRestricted">
|
<div class="dashboard-column d-flex flex-column gap-2">
|
||||||
<div class="detail-box">
|
<div class="detail-box">
|
||||||
<div class="box-header justify-content-center">
|
<div class="box-header justify-content-center">
|
||||||
<span><i class="bi bi-sliders me-2"></i> Gestão</span>
|
<span><i class="bi bi-sliders me-2"></i> Gestão</span>
|
||||||
|
|
@ -1639,7 +1632,7 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="dashboard-column" *ngIf="!isClientRestricted">
|
<div class="dashboard-column">
|
||||||
<div class="detail-box h-100">
|
<div class="detail-box h-100">
|
||||||
<div class="box-header justify-content-center">
|
<div class="box-header justify-content-center">
|
||||||
<span><i class="bi bi-file-earmark-text me-2"></i> Contrato & Status</span>
|
<span><i class="bi bi-file-earmark-text me-2"></i> Contrato & Status</span>
|
||||||
|
|
|
||||||
|
|
@ -205,8 +205,6 @@ export class Geral implements OnInit, AfterViewInit, OnDestroy {
|
||||||
})();
|
})();
|
||||||
loading = false;
|
loading = false;
|
||||||
isAdmin = false;
|
isAdmin = false;
|
||||||
isGestor = false;
|
|
||||||
isClientRestricted = false;
|
|
||||||
|
|
||||||
rows: LineRow[] = [];
|
rows: LineRow[] = [];
|
||||||
clientGroups: ClientGroupDto[] = [];
|
clientGroups: ClientGroupDto[] = [];
|
||||||
|
|
@ -546,16 +544,7 @@ export class Geral implements OnInit, AfterViewInit, OnDestroy {
|
||||||
|
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
if (!isPlatformBrowser(this.platformId)) return;
|
if (!isPlatformBrowser(this.platformId)) return;
|
||||||
this.isAdmin = this.authService.hasRole('sysadmin');
|
this.isAdmin = this.authService.hasRole('admin');
|
||||||
this.isGestor = this.authService.hasRole('gestor');
|
|
||||||
this.isClientRestricted = !(this.isAdmin || this.isGestor);
|
|
||||||
|
|
||||||
if (this.isClientRestricted) {
|
|
||||||
this.filterSkil = 'ALL';
|
|
||||||
this.additionalMode = 'ALL';
|
|
||||||
this.selectedAdditionalServices = [];
|
|
||||||
this.selectedClients = [];
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async ngAfterViewInit() {
|
async ngAfterViewInit() {
|
||||||
|
|
@ -564,9 +553,7 @@ export class Geral implements OnInit, AfterViewInit, OnDestroy {
|
||||||
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
this.refreshData();
|
this.refreshData();
|
||||||
if (!this.isClientRestricted) {
|
this.loadClients();
|
||||||
this.loadClients();
|
|
||||||
}
|
|
||||||
this.loadPlanRules();
|
this.loadPlanRules();
|
||||||
this.loadAccountCompanies();
|
this.loadAccountCompanies();
|
||||||
|
|
||||||
|
|
@ -587,9 +574,7 @@ export class Geral implements OnInit, AfterViewInit, OnDestroy {
|
||||||
if (!url.includes('/geral')) return;
|
if (!url.includes('/geral')) return;
|
||||||
|
|
||||||
this.searchResolvedClient = null;
|
this.searchResolvedClient = null;
|
||||||
if (!this.isClientRestricted) {
|
this.loadClients();
|
||||||
this.loadClients();
|
|
||||||
}
|
|
||||||
this.refreshData();
|
this.refreshData();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
@ -930,8 +915,6 @@ export class Geral implements OnInit, AfterViewInit, OnDestroy {
|
||||||
}
|
}
|
||||||
|
|
||||||
setFilter(type: 'ALL' | 'PF' | 'PJ' | 'RESERVA') {
|
setFilter(type: 'ALL' | 'PF' | 'PJ' | 'RESERVA') {
|
||||||
if (this.isClientRestricted && type !== 'ALL') return;
|
|
||||||
|
|
||||||
const isSameFilter = this.filterSkil === type;
|
const isSameFilter = this.filterSkil === type;
|
||||||
|
|
||||||
this.expandedGroup = null;
|
this.expandedGroup = null;
|
||||||
|
|
@ -944,16 +927,13 @@ export class Geral implements OnInit, AfterViewInit, OnDestroy {
|
||||||
this.clientSearchTerm = '';
|
this.clientSearchTerm = '';
|
||||||
this.searchResolvedClient = null;
|
this.searchResolvedClient = null;
|
||||||
|
|
||||||
if (!this.isClientRestricted) {
|
this.loadClients();
|
||||||
this.loadClients();
|
|
||||||
}
|
|
||||||
|
|
||||||
this.page = 1;
|
this.page = 1;
|
||||||
this.refreshData();
|
this.refreshData();
|
||||||
}
|
}
|
||||||
|
|
||||||
setAdditionalMode(mode: AdditionalMode) {
|
setAdditionalMode(mode: AdditionalMode) {
|
||||||
if (this.isClientRestricted) return;
|
|
||||||
if (this.additionalMode === mode) return;
|
if (this.additionalMode === mode) return;
|
||||||
|
|
||||||
this.additionalMode = mode;
|
this.additionalMode = mode;
|
||||||
|
|
@ -967,7 +947,6 @@ export class Geral implements OnInit, AfterViewInit, OnDestroy {
|
||||||
}
|
}
|
||||||
|
|
||||||
toggleAdditionalService(key: AdditionalServiceKey) {
|
toggleAdditionalService(key: AdditionalServiceKey) {
|
||||||
if (this.isClientRestricted) return;
|
|
||||||
const idx = this.selectedAdditionalServices.indexOf(key);
|
const idx = this.selectedAdditionalServices.indexOf(key);
|
||||||
if (idx >= 0) this.selectedAdditionalServices.splice(idx, 1);
|
if (idx >= 0) this.selectedAdditionalServices.splice(idx, 1);
|
||||||
else this.selectedAdditionalServices.push(key);
|
else this.selectedAdditionalServices.push(key);
|
||||||
|
|
@ -986,7 +965,6 @@ export class Geral implements OnInit, AfterViewInit, OnDestroy {
|
||||||
}
|
}
|
||||||
|
|
||||||
clearAdditionalFilters() {
|
clearAdditionalFilters() {
|
||||||
if (this.isClientRestricted) return;
|
|
||||||
this.additionalMode = 'ALL';
|
this.additionalMode = 'ALL';
|
||||||
this.selectedAdditionalServices = [];
|
this.selectedAdditionalServices = [];
|
||||||
this.expandedGroup = null;
|
this.expandedGroup = null;
|
||||||
|
|
@ -1449,13 +1427,11 @@ export class Geral implements OnInit, AfterViewInit, OnDestroy {
|
||||||
}
|
}
|
||||||
|
|
||||||
toggleClientMenu() {
|
toggleClientMenu() {
|
||||||
if (this.isClientRestricted) return;
|
|
||||||
if (!this.showClientMenu) this.showAdditionalMenu = false;
|
if (!this.showClientMenu) this.showAdditionalMenu = false;
|
||||||
this.showClientMenu = !this.showClientMenu;
|
this.showClientMenu = !this.showClientMenu;
|
||||||
}
|
}
|
||||||
|
|
||||||
toggleAdditionalMenu() {
|
toggleAdditionalMenu() {
|
||||||
if (this.isClientRestricted) return;
|
|
||||||
if (!this.showAdditionalMenu) this.showClientMenu = false;
|
if (!this.showAdditionalMenu) this.showClientMenu = false;
|
||||||
this.showAdditionalMenu = !this.showAdditionalMenu;
|
this.showAdditionalMenu = !this.showAdditionalMenu;
|
||||||
}
|
}
|
||||||
|
|
@ -1474,7 +1450,6 @@ export class Geral implements OnInit, AfterViewInit, OnDestroy {
|
||||||
}
|
}
|
||||||
|
|
||||||
selectClient(client: string | null) {
|
selectClient(client: string | null) {
|
||||||
if (this.isClientRestricted) return;
|
|
||||||
if (client === null) {
|
if (client === null) {
|
||||||
this.selectedClients = [];
|
this.selectedClients = [];
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -1490,7 +1465,6 @@ export class Geral implements OnInit, AfterViewInit, OnDestroy {
|
||||||
}
|
}
|
||||||
|
|
||||||
removeClient(client: string, event: Event) {
|
removeClient(client: string, event: Event) {
|
||||||
if (this.isClientRestricted) return;
|
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
const idx = this.selectedClients.indexOf(client);
|
const idx = this.selectedClients.indexOf(client);
|
||||||
if (idx >= 0) {
|
if (idx >= 0) {
|
||||||
|
|
@ -1503,7 +1477,6 @@ export class Geral implements OnInit, AfterViewInit, OnDestroy {
|
||||||
}
|
}
|
||||||
|
|
||||||
clearClientSelection(event?: Event) {
|
clearClientSelection(event?: Event) {
|
||||||
if (this.isClientRestricted) return;
|
|
||||||
if (event) event.stopPropagation();
|
if (event) event.stopPropagation();
|
||||||
this.selectedClients = [];
|
this.selectedClients = [];
|
||||||
this.clientSearchTerm = '';
|
this.clientSearchTerm = '';
|
||||||
|
|
@ -1803,7 +1776,7 @@ export class Geral implements OnInit, AfterViewInit, OnDestroy {
|
||||||
|
|
||||||
async onRemover(r: LineRow, fromGroup = false) {
|
async onRemover(r: LineRow, fromGroup = false) {
|
||||||
if (!this.isAdmin) {
|
if (!this.isAdmin) {
|
||||||
await this.showToast('Apenas sysadmin pode remover linhas.');
|
await this.showToast('Apenas administradores podem remover linhas.');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1835,11 +1808,6 @@ export class Geral implements OnInit, AfterViewInit, OnDestroy {
|
||||||
}
|
}
|
||||||
|
|
||||||
async onCadastrarLinha() {
|
async onCadastrarLinha() {
|
||||||
if (this.isClientRestricted) {
|
|
||||||
await this.showToast('Você não tem permissão para cadastrar novos clientes.');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.createMode = 'NEW_CLIENT';
|
this.createMode = 'NEW_CLIENT';
|
||||||
this.resetCreateModel();
|
this.resetCreateModel();
|
||||||
this.createOpen = true;
|
this.createOpen = true;
|
||||||
|
|
@ -1847,11 +1815,6 @@ export class Geral implements OnInit, AfterViewInit, OnDestroy {
|
||||||
}
|
}
|
||||||
|
|
||||||
async onAddLineToGroup(clientName: string) {
|
async onAddLineToGroup(clientName: string) {
|
||||||
if (this.isClientRestricted) {
|
|
||||||
await this.showToast('Você não tem permissão para adicionar linhas.');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.createMode = 'NEW_LINE_IN_GROUP';
|
this.createMode = 'NEW_LINE_IN_GROUP';
|
||||||
this.resetCreateModel();
|
this.resetCreateModel();
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -158,7 +158,7 @@ export class Parcelamentos implements OnInit, OnDestroy {
|
||||||
}
|
}
|
||||||
|
|
||||||
private syncPermissions(): void {
|
private syncPermissions(): void {
|
||||||
this.isAdmin = this.authService.hasRole('sysadmin');
|
this.isAdmin = this.authService.hasRole('admin');
|
||||||
}
|
}
|
||||||
|
|
||||||
get totalPages(): number {
|
get totalPages(): number {
|
||||||
|
|
|
||||||
|
|
@ -1,146 +0,0 @@
|
||||||
<section class="system-provision-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>
|
|
||||||
|
|
||||||
<div class="container-shell">
|
|
||||||
<div class="card-shell">
|
|
||||||
<header class="card-header">
|
|
||||||
<div class="title-badge">
|
|
||||||
<i class="bi bi-shield-lock-fill"></i> SYSTEM ADMIN
|
|
||||||
</div>
|
|
||||||
<h1>Fornecer Usuário para Cliente</h1>
|
|
||||||
<p>Selecione um tenant-cliente e crie credenciais de acesso sem misturar tenants.</p>
|
|
||||||
</header>
|
|
||||||
|
|
||||||
<div class="card-body">
|
|
||||||
<div class="alert-box error" *ngIf="tenantsError">
|
|
||||||
{{ tenantsError }}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="alert-box success" *ngIf="successMessage">
|
|
||||||
{{ successMessage }}
|
|
||||||
<div class="mt-1" *ngIf="createdUser">
|
|
||||||
<small>
|
|
||||||
UserId: <strong>{{ createdUser.userId }}</strong> | TenantId:
|
|
||||||
<strong>{{ createdUser.tenantId }}</strong>
|
|
||||||
</small>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="alert-box error" *ngIf="submitErrors.length">
|
|
||||||
<strong>Falha ao criar usuário:</strong>
|
|
||||||
<ul>
|
|
||||||
<li *ngFor="let err of submitErrors">{{ err }}</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<form [formGroup]="provisionForm" (ngSubmit)="onSubmit()" class="provision-form" novalidate>
|
|
||||||
<div class="form-grid">
|
|
||||||
<div class="form-field span-2">
|
|
||||||
<label for="tenantId">Cliente (Tenant)</label>
|
|
||||||
<div class="select-row">
|
|
||||||
<select
|
|
||||||
id="tenantId"
|
|
||||||
formControlName="tenantId"
|
|
||||||
class="form-control"
|
|
||||||
[disabled]="tenantsLoading || !tenants.length"
|
|
||||||
>
|
|
||||||
<option value="">Selecione um cliente...</option>
|
|
||||||
<option
|
|
||||||
*ngFor="let tenant of tenants; trackBy: trackByTenantId"
|
|
||||||
[value]="tenant.tenantId"
|
|
||||||
>
|
|
||||||
{{ tenant.nomeOficial }}
|
|
||||||
</option>
|
|
||||||
</select>
|
|
||||||
<button type="button" class="btn btn-ghost" (click)="loadTenants()" [disabled]="tenantsLoading">
|
|
||||||
{{ tenantsLoading ? 'Atualizando...' : 'Atualizar lista' }}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<small class="field-help">Origem: {{ sourceType }} (apenas tenants ativos).</small>
|
|
||||||
<small class="field-error" *ngIf="hasFieldError('tenantId', 'required')">
|
|
||||||
Selecione um tenant-cliente.
|
|
||||||
</small>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="form-field">
|
|
||||||
<label for="name">Nome (opcional)</label>
|
|
||||||
<input id="name" type="text" class="form-control" formControlName="name" placeholder="Nome do usuário" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="form-field">
|
|
||||||
<label for="email">Email</label>
|
|
||||||
<input
|
|
||||||
id="email"
|
|
||||||
type="email"
|
|
||||||
class="form-control"
|
|
||||||
formControlName="email"
|
|
||||||
placeholder="usuario@cliente.com"
|
|
||||||
/>
|
|
||||||
<small class="field-error" *ngIf="hasFieldError('email', 'required')">Email é obrigatório.</small>
|
|
||||||
<small class="field-error" *ngIf="hasFieldError('email', 'email')">Email inválido.</small>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="form-field">
|
|
||||||
<label for="password">Senha</label>
|
|
||||||
<input
|
|
||||||
id="password"
|
|
||||||
type="password"
|
|
||||||
class="form-control"
|
|
||||||
formControlName="password"
|
|
||||||
placeholder="Defina uma senha"
|
|
||||||
autocomplete="new-password"
|
|
||||||
/>
|
|
||||||
<small class="field-error" *ngIf="hasFieldError('password', 'required')">Senha é obrigatória.</small>
|
|
||||||
<small class="field-error" *ngIf="hasFieldError('password', 'minlength')">Mínimo de 6 caracteres.</small>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="form-field">
|
|
||||||
<label for="confirmPassword">Confirmar senha</label>
|
|
||||||
<input
|
|
||||||
id="confirmPassword"
|
|
||||||
type="password"
|
|
||||||
class="form-control"
|
|
||||||
formControlName="confirmPassword"
|
|
||||||
placeholder="Repita a senha"
|
|
||||||
autocomplete="new-password"
|
|
||||||
/>
|
|
||||||
<small class="field-error" *ngIf="hasFieldError('confirmPassword', 'required')">
|
|
||||||
Confirmação é obrigatória.
|
|
||||||
</small>
|
|
||||||
<small class="field-error" *ngIf="passwordMismatch">As senhas não conferem.</small>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="form-field span-2">
|
|
||||||
<label>Roles do usuário</label>
|
|
||||||
<div class="roles-grid">
|
|
||||||
<label class="role-item" *ngFor="let role of roleOptions; trackBy: trackByRoleValue">
|
|
||||||
<input
|
|
||||||
type="checkbox"
|
|
||||||
[checked]="isRoleSelected(role.value)"
|
|
||||||
(change)="toggleRole(role.value, $any($event.target).checked)"
|
|
||||||
/>
|
|
||||||
<div class="role-content">
|
|
||||||
<strong>{{ role.label }}</strong>
|
|
||||||
<span>{{ role.description }}</span>
|
|
||||||
</div>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
<small class="field-error" *ngIf="selectedRoles.length === 0">
|
|
||||||
Selecione ao menos uma role.
|
|
||||||
</small>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="form-actions">
|
|
||||||
<button type="submit" class="btn btn-primary" [disabled]="submitting || provisionForm.invalid">
|
|
||||||
<span *ngIf="!submitting">Criar usuário para cliente</span>
|
|
||||||
<span *ngIf="submitting">Criando...</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
@ -1,298 +0,0 @@
|
||||||
:host {
|
|
||||||
--brand: #e33dcf;
|
|
||||||
--brand-dark: #8b2d7f;
|
|
||||||
--text: #111214;
|
|
||||||
--muted: rgba(17, 18, 20, 0.66);
|
|
||||||
--danger: #dc3545;
|
|
||||||
--success: #198754;
|
|
||||||
--border: rgba(17, 18, 20, 0.12);
|
|
||||||
--card-bg: rgba(255, 255, 255, 0.84);
|
|
||||||
--surface: #ffffff;
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
|
|
||||||
.system-provision-page {
|
|
||||||
min-height: 100vh;
|
|
||||||
padding: 24px 12px 110px;
|
|
||||||
position: relative;
|
|
||||||
background:
|
|
||||||
radial-gradient(780px 360px at 10% 0%, rgba(227, 61, 207, 0.16), transparent 60%),
|
|
||||||
radial-gradient(900px 380px at 90% 16%, rgba(3, 15, 170, 0.12), transparent 62%),
|
|
||||||
linear-gradient(180deg, #ffffff 0%, #f4f6fb 100%);
|
|
||||||
}
|
|
||||||
|
|
||||||
.page-blob {
|
|
||||||
position: fixed;
|
|
||||||
pointer-events: none;
|
|
||||||
border-radius: 999px;
|
|
||||||
filter: blur(36px);
|
|
||||||
opacity: 0.5;
|
|
||||||
z-index: 0;
|
|
||||||
background: radial-gradient(circle at 40% 40%, rgba(227, 61, 207, 0.4), rgba(227, 61, 207, 0.08));
|
|
||||||
|
|
||||||
&.blob-1 {
|
|
||||||
width: 420px;
|
|
||||||
height: 420px;
|
|
||||||
top: -150px;
|
|
||||||
left: -160px;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.blob-2 {
|
|
||||||
width: 560px;
|
|
||||||
height: 560px;
|
|
||||||
top: -230px;
|
|
||||||
right: -260px;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.blob-3 {
|
|
||||||
width: 360px;
|
|
||||||
height: 360px;
|
|
||||||
bottom: -160px;
|
|
||||||
left: 30%;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.container-shell {
|
|
||||||
width: 100%;
|
|
||||||
max-width: 1080px;
|
|
||||||
margin: 0 auto;
|
|
||||||
position: relative;
|
|
||||||
z-index: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.card-shell {
|
|
||||||
background: var(--card-bg);
|
|
||||||
border: 1px solid rgba(227, 61, 207, 0.2);
|
|
||||||
backdrop-filter: blur(10px);
|
|
||||||
border-radius: 24px;
|
|
||||||
box-shadow: 0 24px 50px rgba(17, 18, 20, 0.12);
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.card-header {
|
|
||||||
padding: 24px 28px 18px;
|
|
||||||
border-bottom: 1px solid rgba(17, 18, 20, 0.08);
|
|
||||||
|
|
||||||
h1 {
|
|
||||||
margin: 10px 0 6px;
|
|
||||||
font-size: clamp(1.4rem, 2.2vw, 2rem);
|
|
||||||
font-weight: 900;
|
|
||||||
letter-spacing: -0.02em;
|
|
||||||
color: var(--text);
|
|
||||||
}
|
|
||||||
|
|
||||||
p {
|
|
||||||
margin: 0;
|
|
||||||
color: var(--muted);
|
|
||||||
font-size: 0.95rem;
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.title-badge {
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 10px;
|
|
||||||
border-radius: 999px;
|
|
||||||
padding: 6px 12px;
|
|
||||||
background: rgba(255, 255, 255, 0.82);
|
|
||||||
border: 1px solid rgba(227, 61, 207, 0.24);
|
|
||||||
font-size: 0.78rem;
|
|
||||||
font-weight: 800;
|
|
||||||
letter-spacing: 0.05em;
|
|
||||||
color: var(--brand-dark);
|
|
||||||
|
|
||||||
i {
|
|
||||||
color: var(--brand);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.card-body {
|
|
||||||
padding: 20px 28px 28px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.alert-box {
|
|
||||||
border-radius: 12px;
|
|
||||||
padding: 10px 12px;
|
|
||||||
font-size: 0.9rem;
|
|
||||||
font-weight: 600;
|
|
||||||
margin-bottom: 12px;
|
|
||||||
|
|
||||||
ul {
|
|
||||||
margin: 8px 0 0 18px;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.error {
|
|
||||||
color: var(--danger);
|
|
||||||
background: rgba(220, 53, 69, 0.1);
|
|
||||||
border: 1px solid rgba(220, 53, 69, 0.22);
|
|
||||||
}
|
|
||||||
|
|
||||||
&.success {
|
|
||||||
color: var(--success);
|
|
||||||
background: rgba(25, 135, 84, 0.1);
|
|
||||||
border: 1px solid rgba(25, 135, 84, 0.24);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.provision-form {
|
|
||||||
display: grid;
|
|
||||||
gap: 18px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-grid {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
||||||
gap: 14px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-field {
|
|
||||||
display: grid;
|
|
||||||
gap: 6px;
|
|
||||||
|
|
||||||
&.span-2 {
|
|
||||||
grid-column: span 2;
|
|
||||||
}
|
|
||||||
|
|
||||||
label {
|
|
||||||
margin: 0;
|
|
||||||
font-size: 0.8rem;
|
|
||||||
font-weight: 800;
|
|
||||||
color: var(--text);
|
|
||||||
text-transform: uppercase;
|
|
||||||
letter-spacing: 0.03em;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.select-row {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: minmax(0, 1fr) auto;
|
|
||||||
gap: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-control {
|
|
||||||
width: 100%;
|
|
||||||
height: 42px;
|
|
||||||
border-radius: 10px;
|
|
||||||
border: 1px solid var(--border);
|
|
||||||
background: var(--surface);
|
|
||||||
color: var(--text);
|
|
||||||
padding: 0 12px;
|
|
||||||
font-size: 0.92rem;
|
|
||||||
transition: border-color 0.15s ease, box-shadow 0.15s ease;
|
|
||||||
|
|
||||||
&:focus {
|
|
||||||
outline: none;
|
|
||||||
border-color: var(--brand);
|
|
||||||
box-shadow: 0 0 0 3px rgba(227, 61, 207, 0.14);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.field-help {
|
|
||||||
font-size: 0.78rem;
|
|
||||||
color: var(--muted);
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
|
|
||||||
.field-error {
|
|
||||||
font-size: 0.78rem;
|
|
||||||
color: var(--danger);
|
|
||||||
font-weight: 700;
|
|
||||||
}
|
|
||||||
|
|
||||||
.roles-grid {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
||||||
gap: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.role-item {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: auto minmax(0, 1fr);
|
|
||||||
gap: 10px;
|
|
||||||
border: 1px solid rgba(17, 18, 20, 0.1);
|
|
||||||
border-radius: 12px;
|
|
||||||
padding: 10px;
|
|
||||||
background: rgba(255, 255, 255, 0.86);
|
|
||||||
cursor: pointer;
|
|
||||||
|
|
||||||
input {
|
|
||||||
margin-top: 4px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.role-content {
|
|
||||||
display: grid;
|
|
||||||
gap: 2px;
|
|
||||||
|
|
||||||
strong {
|
|
||||||
font-size: 0.88rem;
|
|
||||||
color: var(--text);
|
|
||||||
}
|
|
||||||
|
|
||||||
span {
|
|
||||||
font-size: 0.78rem;
|
|
||||||
color: var(--muted);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-actions {
|
|
||||||
display: flex;
|
|
||||||
justify-content: flex-end;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn {
|
|
||||||
border: 1px solid transparent;
|
|
||||||
border-radius: 10px;
|
|
||||||
padding: 0 14px;
|
|
||||||
height: 40px;
|
|
||||||
font-size: 0.88rem;
|
|
||||||
font-weight: 700;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-primary {
|
|
||||||
background: linear-gradient(120deg, #e33dcf, #b131a0);
|
|
||||||
color: #fff;
|
|
||||||
|
|
||||||
&:disabled {
|
|
||||||
opacity: 0.65;
|
|
||||||
cursor: not-allowed;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-ghost {
|
|
||||||
border-color: rgba(17, 18, 20, 0.16);
|
|
||||||
background: #fff;
|
|
||||||
color: var(--text);
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 992px) {
|
|
||||||
.card-header,
|
|
||||||
.card-body {
|
|
||||||
padding-left: 18px;
|
|
||||||
padding-right: 18px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.roles-grid {
|
|
||||||
grid-template-columns: 1fr;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 768px) {
|
|
||||||
.system-provision-page {
|
|
||||||
padding-top: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-grid {
|
|
||||||
grid-template-columns: 1fr;
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-field.span-2 {
|
|
||||||
grid-column: span 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.select-row {
|
|
||||||
grid-template-columns: 1fr;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,255 +0,0 @@
|
||||||
import { Component, OnInit } from '@angular/core';
|
|
||||||
import { CommonModule } from '@angular/common';
|
|
||||||
import { HttpErrorResponse } from '@angular/common/http';
|
|
||||||
import {
|
|
||||||
AbstractControl,
|
|
||||||
FormBuilder,
|
|
||||||
FormGroup,
|
|
||||||
ReactiveFormsModule,
|
|
||||||
ValidationErrors,
|
|
||||||
Validators,
|
|
||||||
} from '@angular/forms';
|
|
||||||
|
|
||||||
import {
|
|
||||||
SystemAdminService,
|
|
||||||
SystemTenantDto,
|
|
||||||
CreateSystemTenantUserResponse,
|
|
||||||
} from '../../services/system-admin.service';
|
|
||||||
|
|
||||||
type RoleOption = {
|
|
||||||
value: string;
|
|
||||||
label: string;
|
|
||||||
description: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
@Component({
|
|
||||||
selector: 'app-system-provision-user',
|
|
||||||
standalone: true,
|
|
||||||
imports: [CommonModule, ReactiveFormsModule],
|
|
||||||
templateUrl: './system-provision-user.html',
|
|
||||||
styleUrls: ['./system-provision-user.scss'],
|
|
||||||
})
|
|
||||||
export class SystemProvisionUserPage implements OnInit {
|
|
||||||
readonly roleOptions: RoleOption[] = [
|
|
||||||
{ value: 'sysadmin', label: 'SysAdmin', description: 'Acesso administrativo global do sistema (apenas SystemTenant).' },
|
|
||||||
{ value: 'gestor', label: 'Gestor', description: 'Acesso global de gestão, sem permissões administrativas.' },
|
|
||||||
{ value: 'cliente', label: 'Cliente', description: 'Acesso restrito ao tenant do cliente.' },
|
|
||||||
];
|
|
||||||
|
|
||||||
readonly sourceType = 'MobileLines.Cliente';
|
|
||||||
|
|
||||||
provisionForm: FormGroup;
|
|
||||||
tenants: SystemTenantDto[] = [];
|
|
||||||
tenantsLoading = false;
|
|
||||||
tenantsError = '';
|
|
||||||
|
|
||||||
submitting = false;
|
|
||||||
submitErrors: string[] = [];
|
|
||||||
successMessage = '';
|
|
||||||
createdUser: CreateSystemTenantUserResponse | null = null;
|
|
||||||
|
|
||||||
constructor(
|
|
||||||
private fb: FormBuilder,
|
|
||||||
private systemAdminService: SystemAdminService
|
|
||||||
) {
|
|
||||||
this.provisionForm = this.fb.group(
|
|
||||||
{
|
|
||||||
tenantId: ['', [Validators.required]],
|
|
||||||
name: [''],
|
|
||||||
email: ['', [Validators.required, Validators.email]],
|
|
||||||
password: ['', [Validators.required, Validators.minLength(6)]],
|
|
||||||
confirmPassword: ['', [Validators.required, Validators.minLength(6)]],
|
|
||||||
roles: this.fb.control<string[]>(['cliente'], [Validators.required]),
|
|
||||||
},
|
|
||||||
{ validators: this.passwordsMatchValidator }
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
ngOnInit(): void {
|
|
||||||
this.loadTenants();
|
|
||||||
}
|
|
||||||
|
|
||||||
loadTenants(): void {
|
|
||||||
if (this.tenantsLoading) return;
|
|
||||||
|
|
||||||
this.tenantsLoading = true;
|
|
||||||
this.tenantsError = '';
|
|
||||||
|
|
||||||
this.systemAdminService
|
|
||||||
.listTenants({ source: this.sourceType, active: true })
|
|
||||||
.subscribe({
|
|
||||||
next: (tenants) => {
|
|
||||||
this.tenants = (tenants || []).slice().sort((a, b) =>
|
|
||||||
(a.nomeOficial || '').localeCompare(b.nomeOficial || '', 'pt-BR', { sensitivity: 'base' })
|
|
||||||
);
|
|
||||||
this.tenantsLoading = false;
|
|
||||||
},
|
|
||||||
error: (err: HttpErrorResponse) => {
|
|
||||||
this.tenantsLoading = false;
|
|
||||||
this.tenantsError = this.extractErrorMessage(
|
|
||||||
err,
|
|
||||||
'Não foi possível carregar os clientes. Verifique se a conta possui role sysadmin.'
|
|
||||||
);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
isRoleSelected(role: string): boolean {
|
|
||||||
const selected = this.selectedRoles;
|
|
||||||
return selected.includes(role);
|
|
||||||
}
|
|
||||||
|
|
||||||
toggleRole(role: string, checked: boolean): void {
|
|
||||||
const current = this.selectedRoles;
|
|
||||||
const next = checked
|
|
||||||
? Array.from(new Set([...current, role]))
|
|
||||||
: current.filter((value) => value !== role);
|
|
||||||
|
|
||||||
this.rolesControl.setValue(next);
|
|
||||||
this.rolesControl.markAsDirty();
|
|
||||||
this.rolesControl.markAsTouched();
|
|
||||||
}
|
|
||||||
|
|
||||||
onSubmit(): void {
|
|
||||||
if (this.submitting) return;
|
|
||||||
|
|
||||||
this.successMessage = '';
|
|
||||||
this.submitErrors = [];
|
|
||||||
this.createdUser = null;
|
|
||||||
|
|
||||||
if (this.provisionForm.invalid || this.selectedRoles.length === 0) {
|
|
||||||
this.provisionForm.markAllAsTouched();
|
|
||||||
if (this.selectedRoles.length === 0) {
|
|
||||||
this.submitErrors = ['Selecione ao menos uma role para o usuário.'];
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const tenantId = String(this.provisionForm.get('tenantId')?.value ?? '').trim();
|
|
||||||
const email = String(this.provisionForm.get('email')?.value ?? '').trim().toLowerCase();
|
|
||||||
const nameRaw = String(this.provisionForm.get('name')?.value ?? '').trim();
|
|
||||||
const password = String(this.provisionForm.get('password')?.value ?? '');
|
|
||||||
|
|
||||||
this.submitting = true;
|
|
||||||
this.setFormDisabled(true);
|
|
||||||
|
|
||||||
this.systemAdminService
|
|
||||||
.createTenantUser(tenantId, {
|
|
||||||
name: nameRaw,
|
|
||||||
email,
|
|
||||||
password,
|
|
||||||
roles: this.selectedRoles,
|
|
||||||
})
|
|
||||||
.subscribe({
|
|
||||||
next: (created) => {
|
|
||||||
this.submitting = false;
|
|
||||||
this.setFormDisabled(false);
|
|
||||||
|
|
||||||
this.createdUser = created;
|
|
||||||
const tenant = this.findTenantById(created.tenantId) ?? this.findTenantById(tenantId);
|
|
||||||
const tenantName = tenant?.nomeOficial || 'cliente selecionado';
|
|
||||||
this.successMessage = `Usuário ${created.email} criado com sucesso para ${tenantName}.`;
|
|
||||||
|
|
||||||
this.provisionForm.patchValue({
|
|
||||||
name: '',
|
|
||||||
email: '',
|
|
||||||
password: '',
|
|
||||||
confirmPassword: '',
|
|
||||||
roles: ['cliente'],
|
|
||||||
});
|
|
||||||
this.provisionForm.markAsPristine();
|
|
||||||
this.provisionForm.markAsUntouched();
|
|
||||||
},
|
|
||||||
error: (err: HttpErrorResponse) => {
|
|
||||||
this.submitting = false;
|
|
||||||
this.setFormDisabled(false);
|
|
||||||
this.submitErrors = this.extractErrors(err);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
trackByTenantId(_: number, tenant: SystemTenantDto): string {
|
|
||||||
return tenant.tenantId;
|
|
||||||
}
|
|
||||||
|
|
||||||
trackByRoleValue(_: number, role: RoleOption): string {
|
|
||||||
return role.value;
|
|
||||||
}
|
|
||||||
|
|
||||||
hasFieldError(field: string, error?: string): boolean {
|
|
||||||
const control = this.provisionForm.get(field);
|
|
||||||
if (!control) return false;
|
|
||||||
if (error) return !!(control.touched && control.hasError(error));
|
|
||||||
return !!(control.touched && control.invalid);
|
|
||||||
}
|
|
||||||
|
|
||||||
get passwordMismatch(): boolean {
|
|
||||||
const confirmTouched = this.provisionForm.get('confirmPassword')?.touched;
|
|
||||||
return !!(confirmTouched && this.provisionForm.errors?.['passwordMismatch']);
|
|
||||||
}
|
|
||||||
|
|
||||||
get selectedRoles(): string[] {
|
|
||||||
const roles = this.rolesControl.value;
|
|
||||||
return Array.isArray(roles) ? roles : [];
|
|
||||||
}
|
|
||||||
|
|
||||||
get rolesControl(): AbstractControl<string[] | null, string[] | null> {
|
|
||||||
return this.provisionForm.get('roles') as AbstractControl<string[] | null, string[] | null>;
|
|
||||||
}
|
|
||||||
|
|
||||||
private findTenantById(tenantId: string): SystemTenantDto | undefined {
|
|
||||||
return this.tenants.find((tenant) => tenant.tenantId === tenantId);
|
|
||||||
}
|
|
||||||
|
|
||||||
private setFormDisabled(disabled: boolean): void {
|
|
||||||
if (disabled) {
|
|
||||||
this.provisionForm.disable({ emitEvent: false });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.provisionForm.enable({ emitEvent: false });
|
|
||||||
}
|
|
||||||
|
|
||||||
private extractErrors(err: HttpErrorResponse): string[] {
|
|
||||||
const apiError = err?.error;
|
|
||||||
|
|
||||||
if (Array.isArray(apiError)) {
|
|
||||||
const list = apiError.map((entry) => String(entry ?? '').trim()).filter(Boolean);
|
|
||||||
if (list.length) return list;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Array.isArray(apiError?.errors)) {
|
|
||||||
const list = apiError.errors
|
|
||||||
.map((entry: unknown) => String((entry as { message?: string })?.message ?? entry ?? '').trim())
|
|
||||||
.filter(Boolean);
|
|
||||||
if (list.length) return list;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (typeof apiError?.message === 'string' && apiError.message.trim()) {
|
|
||||||
return [apiError.message.trim()];
|
|
||||||
}
|
|
||||||
|
|
||||||
if (typeof apiError === 'string' && apiError.trim()) {
|
|
||||||
return [apiError.trim()];
|
|
||||||
}
|
|
||||||
|
|
||||||
if (err.status === 403) {
|
|
||||||
return ['Acesso negado. Este recurso é exclusivo para sysadmin.'];
|
|
||||||
}
|
|
||||||
|
|
||||||
return ['Não foi possível criar o usuário para o cliente selecionado.'];
|
|
||||||
}
|
|
||||||
|
|
||||||
private extractErrorMessage(err: HttpErrorResponse, fallback: string): string {
|
|
||||||
const messages = this.extractErrors(err);
|
|
||||||
if (messages.length) return messages[0];
|
|
||||||
return fallback;
|
|
||||||
}
|
|
||||||
|
|
||||||
private passwordsMatchValidator(group: AbstractControl): ValidationErrors | null {
|
|
||||||
const password = group.get('password')?.value;
|
|
||||||
const confirm = group.get('confirmPassword')?.value;
|
|
||||||
if (!password || !confirm) return null;
|
|
||||||
return password === confirm ? null : { passwordMismatch: true };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -113,7 +113,7 @@ export class VigenciaComponent implements OnInit, OnDestroy {
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
this.isAdmin = this.authService.hasRole('sysadmin');
|
this.isAdmin = this.authService.hasRole('admin');
|
||||||
this.loadClients();
|
this.loadClients();
|
||||||
this.loadPlanRules();
|
this.loadPlanRules();
|
||||||
this.fetch(1);
|
this.fetch(1);
|
||||||
|
|
|
||||||
|
|
@ -1,63 +0,0 @@
|
||||||
import { Injectable } from '@angular/core';
|
|
||||||
import { HttpClient, HttpParams } from '@angular/common/http';
|
|
||||||
import { Observable } from 'rxjs';
|
|
||||||
|
|
||||||
import { environment } from '../../environments/environment';
|
|
||||||
|
|
||||||
export type SystemTenantDto = {
|
|
||||||
tenantId: string;
|
|
||||||
nomeOficial: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type ListSystemTenantsParams = {
|
|
||||||
source?: string;
|
|
||||||
active?: boolean;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type CreateSystemTenantUserPayload = {
|
|
||||||
name: string;
|
|
||||||
email: string;
|
|
||||||
password: string;
|
|
||||||
roles: string[];
|
|
||||||
};
|
|
||||||
|
|
||||||
export type CreateSystemTenantUserResponse = {
|
|
||||||
userId: string;
|
|
||||||
tenantId: string;
|
|
||||||
email: string;
|
|
||||||
roles: string[];
|
|
||||||
};
|
|
||||||
|
|
||||||
@Injectable({ providedIn: 'root' })
|
|
||||||
export class SystemAdminService {
|
|
||||||
private readonly baseApi: string;
|
|
||||||
|
|
||||||
constructor(private http: HttpClient) {
|
|
||||||
const raw = (environment.apiUrl || '').replace(/\/+$/, '');
|
|
||||||
this.baseApi = raw.toLowerCase().endsWith('/api') ? raw : `${raw}/api`;
|
|
||||||
}
|
|
||||||
|
|
||||||
listTenants(params?: ListSystemTenantsParams): Observable<SystemTenantDto[]> {
|
|
||||||
let httpParams = new HttpParams();
|
|
||||||
if (params?.source) {
|
|
||||||
httpParams = httpParams.set('source', params.source);
|
|
||||||
}
|
|
||||||
if (typeof params?.active === 'boolean') {
|
|
||||||
httpParams = httpParams.set('active', String(params.active));
|
|
||||||
}
|
|
||||||
|
|
||||||
return this.http.get<SystemTenantDto[]>(`${this.baseApi}/system/tenants`, {
|
|
||||||
params: httpParams,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
createTenantUser(
|
|
||||||
tenantId: string,
|
|
||||||
payload: CreateSystemTenantUserPayload
|
|
||||||
): Observable<CreateSystemTenantUserResponse> {
|
|
||||||
return this.http.post<CreateSystemTenantUserResponse>(
|
|
||||||
`${this.baseApi}/system/tenants/${tenantId}/users`,
|
|
||||||
payload
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -4,7 +4,7 @@ import { Observable } from 'rxjs';
|
||||||
|
|
||||||
import { environment } from '../../environments/environment';
|
import { environment } from '../../environments/environment';
|
||||||
|
|
||||||
export type UserPermission = 'sysadmin' | 'gestor' | 'cliente';
|
export type UserPermission = 'admin' | 'gestor';
|
||||||
|
|
||||||
export type UserDto = {
|
export type UserDto = {
|
||||||
id: string;
|
id: string;
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue