Compare commits
No commits in common. "59c2cb828e5db4d7f3c23d0975364d9672da856b" and "875345ea8951e1308c71b848c44b716bfa4669a1" have entirely different histories.
59c2cb828e
...
875345ea89
|
|
@ -43,7 +43,7 @@ export const routes: Routes = [
|
||||||
path: 'system/fornecer-usuario',
|
path: 'system/fornecer-usuario',
|
||||||
component: SystemProvisionUserPage,
|
component: SystemProvisionUserPage,
|
||||||
canActivate: [authGuard, sysadminOnlyGuard],
|
canActivate: [authGuard, sysadminOnlyGuard],
|
||||||
title: 'Criar Credenciais do Cliente',
|
title: 'Fornecer Usuário',
|
||||||
},
|
},
|
||||||
|
|
||||||
// ✅ rota correta
|
// ✅ rota correta
|
||||||
|
|
|
||||||
|
|
@ -11,31 +11,10 @@
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<div class="app-select-panel" *ngIf="isOpen">
|
<div class="app-select-panel" *ngIf="isOpen">
|
||||||
<div class="app-select-search" *ngIf="searchable" (click)="$event.stopPropagation()">
|
|
||||||
<i class="bi bi-search"></i>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
class="app-select-search-input"
|
|
||||||
[value]="searchTerm"
|
|
||||||
[placeholder]="searchPlaceholder"
|
|
||||||
(input)="onSearchInput($any($event.target).value)"
|
|
||||||
(keydown)="onSearchKeydown($event)"
|
|
||||||
/>
|
|
||||||
<button
|
|
||||||
*ngIf="searchTerm"
|
|
||||||
type="button"
|
|
||||||
class="app-select-search-clear"
|
|
||||||
(click)="clearSearch($event)"
|
|
||||||
aria-label="Limpar pesquisa"
|
|
||||||
>
|
|
||||||
<i class="bi bi-x-lg"></i>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="app-select-option"
|
class="app-select-option"
|
||||||
*ngFor="let opt of filteredOptions; trackBy: trackByValue"
|
*ngFor="let opt of options; trackBy: trackByValue"
|
||||||
[class.selected]="isSelected(opt)"
|
[class.selected]="isSelected(opt)"
|
||||||
(click)="selectOption(opt)"
|
(click)="selectOption(opt)"
|
||||||
>
|
>
|
||||||
|
|
@ -43,7 +22,7 @@
|
||||||
<i class="bi bi-check2" *ngIf="isSelected(opt)"></i>
|
<i class="bi bi-check2" *ngIf="isSelected(opt)"></i>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<div class="app-select-empty" *ngIf="!filteredOptions || filteredOptions.length === 0">
|
<div class="app-select-empty" *ngIf="!options || options.length === 0">
|
||||||
Nenhuma opção
|
Nenhuma opção
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -111,59 +111,6 @@
|
||||||
padding: 6px;
|
padding: 6px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.app-select-search {
|
|
||||||
position: sticky;
|
|
||||||
top: 0;
|
|
||||||
z-index: 2;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 6px;
|
|
||||||
margin: 0 0 6px;
|
|
||||||
padding: 6px 8px;
|
|
||||||
border: 1px solid rgba(15, 23, 42, 0.1);
|
|
||||||
border-radius: 9px;
|
|
||||||
background: #fff;
|
|
||||||
|
|
||||||
i {
|
|
||||||
color: #64748b;
|
|
||||||
font-size: 12px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.app-select-search-input {
|
|
||||||
flex: 1 1 auto;
|
|
||||||
min-width: 0;
|
|
||||||
border: none;
|
|
||||||
background: transparent;
|
|
||||||
outline: none;
|
|
||||||
font-size: 12px;
|
|
||||||
color: #0f172a;
|
|
||||||
padding: 0;
|
|
||||||
|
|
||||||
&::placeholder {
|
|
||||||
color: #94a3b8;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.app-select-search-clear {
|
|
||||||
width: 20px;
|
|
||||||
height: 20px;
|
|
||||||
border: none;
|
|
||||||
border-radius: 999px;
|
|
||||||
background: transparent;
|
|
||||||
color: #94a3b8;
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
padding: 0;
|
|
||||||
cursor: pointer;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
background: rgba(148, 163, 184, 0.2);
|
|
||||||
color: #475569;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.app-select-option {
|
.app-select-option {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
border: none;
|
border: none;
|
||||||
|
|
|
||||||
|
|
@ -23,12 +23,9 @@ export class CustomSelectComponent implements ControlValueAccessor {
|
||||||
@Input() valueKey = 'value';
|
@Input() valueKey = 'value';
|
||||||
@Input() size: 'sm' | 'md' = 'md';
|
@Input() size: 'sm' | 'md' = 'md';
|
||||||
@Input() disabled = false;
|
@Input() disabled = false;
|
||||||
@Input() searchable = false;
|
|
||||||
@Input() searchPlaceholder = 'Pesquisar...';
|
|
||||||
|
|
||||||
isOpen = false;
|
isOpen = false;
|
||||||
value: any = null;
|
value: any = null;
|
||||||
searchTerm = '';
|
|
||||||
|
|
||||||
private onChange: (value: any) => void = () => {};
|
private onChange: (value: any) => void = () => {};
|
||||||
private onTouched: () => void = () => {};
|
private onTouched: () => void = () => {};
|
||||||
|
|
@ -66,12 +63,10 @@ export class CustomSelectComponent implements ControlValueAccessor {
|
||||||
toggle(): void {
|
toggle(): void {
|
||||||
if (this.disabled) return;
|
if (this.disabled) return;
|
||||||
this.isOpen = !this.isOpen;
|
this.isOpen = !this.isOpen;
|
||||||
if (!this.isOpen) this.searchTerm = '';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
close(): void {
|
close(): void {
|
||||||
this.isOpen = false;
|
this.isOpen = false;
|
||||||
this.searchTerm = '';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
selectOption(option: any): void {
|
selectOption(option: any): void {
|
||||||
|
|
@ -89,26 +84,6 @@ export class CustomSelectComponent implements ControlValueAccessor {
|
||||||
|
|
||||||
trackByValue = (_: number, option: any) => this.getOptionValue(option);
|
trackByValue = (_: number, option: any) => this.getOptionValue(option);
|
||||||
|
|
||||||
get filteredOptions(): any[] {
|
|
||||||
const opts = this.options || [];
|
|
||||||
const term = this.normalizeText(this.searchTerm);
|
|
||||||
if (!this.searchable || !term) return opts;
|
|
||||||
return opts.filter((opt) => this.normalizeText(this.getOptionLabel(opt)).includes(term));
|
|
||||||
}
|
|
||||||
|
|
||||||
onSearchInput(value: string): void {
|
|
||||||
this.searchTerm = value ?? '';
|
|
||||||
}
|
|
||||||
|
|
||||||
clearSearch(event?: Event): void {
|
|
||||||
event?.stopPropagation();
|
|
||||||
this.searchTerm = '';
|
|
||||||
}
|
|
||||||
|
|
||||||
onSearchKeydown(event: KeyboardEvent): void {
|
|
||||||
event.stopPropagation();
|
|
||||||
}
|
|
||||||
|
|
||||||
private getOptionValue(option: any): any {
|
private getOptionValue(option: any): any {
|
||||||
if (option && typeof option === 'object') {
|
if (option && typeof option === 'object') {
|
||||||
return option[this.valueKey];
|
return option[this.valueKey];
|
||||||
|
|
@ -128,14 +103,6 @@ export class CustomSelectComponent implements ControlValueAccessor {
|
||||||
return (this.options || []).find((o) => this.getOptionValue(o) === value);
|
return (this.options || []).find((o) => this.getOptionValue(o) === value);
|
||||||
}
|
}
|
||||||
|
|
||||||
private normalizeText(value: string): string {
|
|
||||||
return (value ?? '')
|
|
||||||
.normalize('NFD')
|
|
||||||
.replace(/[\u0300-\u036f]/g, '')
|
|
||||||
.toLowerCase()
|
|
||||||
.trim();
|
|
||||||
}
|
|
||||||
|
|
||||||
@HostListener('document:click', ['$event'])
|
@HostListener('document:click', ['$event'])
|
||||||
onDocumentClick(event: MouseEvent): void {
|
onDocumentClick(event: MouseEvent): void {
|
||||||
if (!this.isOpen) return;
|
if (!this.isOpen) return;
|
||||||
|
|
|
||||||
|
|
@ -191,11 +191,8 @@
|
||||||
<button type="button" class="options-item" *ngIf="isSysAdmin" (click)="openManageUsersModal()">
|
<button type="button" class="options-item" *ngIf="isSysAdmin" (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="isSysAdmin" (click)="openManageClientCredentialsModal()">
|
|
||||||
<i class="bi bi-person-badge"></i> Credenciais de clientes
|
|
||||||
</button>
|
|
||||||
<button type="button" class="options-item" *ngIf="isSysAdmin" (click)="goToSystemProvisionUser()">
|
<button type="button" class="options-item" *ngIf="isSysAdmin" (click)="goToSystemProvisionUser()">
|
||||||
<i class="bi bi-shield-lock"></i> Criar credenciais do cliente
|
<i class="bi bi-shield-lock"></i> Fornecer usuário (cliente)
|
||||||
</button>
|
</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()">
|
||||||
|
|
@ -296,7 +293,7 @@
|
||||||
<div class="modal-overlay" *ngIf="manageUsersOpen" (click)="closeManageUsersModal()"></div>
|
<div class="modal-overlay" *ngIf="manageUsersOpen" (click)="closeManageUsersModal()"></div>
|
||||||
<div class="modal-card manage-users-modal" *ngIf="manageUsersOpen" (click)="$event.stopPropagation()">
|
<div class="modal-card manage-users-modal" *ngIf="manageUsersOpen" (click)="$event.stopPropagation()">
|
||||||
<div class="modal-header">
|
<div class="modal-header">
|
||||||
<h3>{{ manageModalTitle }}</h3>
|
<h3>Gestão de Usuários</h3>
|
||||||
<button type="button" class="btn-icon close-x" (click)="closeManageUsersModal()" aria-label="Fechar">
|
<button type="button" class="btn-icon close-x" (click)="closeManageUsersModal()" aria-label="Fechar">
|
||||||
<i class="bi bi-x-lg"></i>
|
<i class="bi bi-x-lg"></i>
|
||||||
</button>
|
</button>
|
||||||
|
|
@ -306,7 +303,7 @@
|
||||||
<div class="manage-search">
|
<div class="manage-search">
|
||||||
<div class="search-input-wrapper">
|
<div class="search-input-wrapper">
|
||||||
<i class="bi bi-search"></i>
|
<i class="bi bi-search"></i>
|
||||||
<input type="text" [placeholder]="manageSearchPlaceholder" [(ngModel)]="manageSearch" (keyup.enter)="onManageSearch()" />
|
<input type="text" placeholder="Buscar por nome ou email..." [(ngModel)]="manageSearch" (keyup.enter)="onManageSearch()" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -319,7 +316,7 @@
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th style="width: 40%;">Usuário</th>
|
<th style="width: 40%;">Usuário</th>
|
||||||
<th style="width: 25%;" class="text-center">Perfil</th>
|
<th style="width: 25%;" class="text-center">Permissão</th>
|
||||||
<th style="width: 15%;" class="text-center">Status</th>
|
<th style="width: 15%;" class="text-center">Status</th>
|
||||||
<th style="width: 20%;" class="text-center">Ações</th>
|
<th style="width: 20%;" class="text-center">Ações</th>
|
||||||
</tr>
|
</tr>
|
||||||
|
|
@ -394,7 +391,7 @@
|
||||||
<div class="avatar-large">{{ target.nome.charAt(0).toUpperCase() }}</div>
|
<div class="avatar-large">{{ target.nome.charAt(0).toUpperCase() }}</div>
|
||||||
<div class="info-text">
|
<div class="info-text">
|
||||||
<h4>{{ target.nome }}</h4>
|
<h4>{{ target.nome }}</h4>
|
||||||
<span>{{ isManageClientsMode ? 'Editando credencial do cliente' : 'Editando perfil' }}</span>
|
<span>Editando perfil</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -406,13 +403,13 @@
|
||||||
<form class="user-form refined-form" id="editUserHeaderForm" [formGroup]="editUserForm" (ngSubmit)="submitEditUser()">
|
<form class="user-form refined-form" id="editUserHeaderForm" [formGroup]="editUserForm" (ngSubmit)="submitEditUser()">
|
||||||
<div class="form-row">
|
<div class="form-row">
|
||||||
<div class="form-field">
|
<div class="form-field">
|
||||||
<label for="editHeaderNome">{{ isManageClientsMode ? 'Nome do responsável' : 'Nome Completo' }}</label>
|
<label for="editHeaderNome">Nome Completo</label>
|
||||||
<input id="editHeaderNome" type="text" formControlName="nome" />
|
<input id="editHeaderNome" type="text" formControlName="nome" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-field">
|
<div class="form-field">
|
||||||
<label for="editHeaderEmail">{{ isManageClientsMode ? 'Email de acesso' : 'Email Corporativo' }}</label>
|
<label for="editHeaderEmail">Email Corporativo</label>
|
||||||
<input id="editHeaderEmail" type="email" formControlName="email" />
|
<input id="editHeaderEmail" type="email" formControlName="email" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -430,13 +427,7 @@
|
||||||
<div class="form-row two-col align-end">
|
<div class="form-row two-col align-end">
|
||||||
<div class="form-field">
|
<div class="form-field">
|
||||||
<label for="editHeaderPermissao">Nível de Acesso</label>
|
<label for="editHeaderPermissao">Nível de Acesso</label>
|
||||||
<app-select
|
<app-select id="editHeaderPermissao" formControlName="permissao" [options]="permissionOptions" labelKey="label" valueKey="value" placeholder="Selecione o nivel"></app-select>
|
||||||
id="editHeaderPermissao"
|
|
||||||
formControlName="permissao"
|
|
||||||
[options]="editPermissionOptions"
|
|
||||||
labelKey="label"
|
|
||||||
valueKey="value"
|
|
||||||
placeholder="Selecione o nivel"></app-select>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-field">
|
<div class="form-field">
|
||||||
|
|
@ -461,7 +452,7 @@
|
||||||
(click)="confirmPermanentDeleteUser(target)"
|
(click)="confirmPermanentDeleteUser(target)"
|
||||||
[disabled]="editUserSubmitting"
|
[disabled]="editUserSubmitting"
|
||||||
[title]="target.ativo !== false ? 'Inative a conta antes de excluir permanentemente' : 'Excluir permanentemente'">
|
[title]="target.ativo !== false ? 'Inative a conta antes de excluir permanentemente' : 'Excluir permanentemente'">
|
||||||
{{ isManageClientsMode ? 'Excluir Credencial' : 'Excluir Permanentemente' }}
|
Excluir Permanentemente
|
||||||
</button>
|
</button>
|
||||||
<button type="button" class="btn-ghost" (click)="cancelEditUser()" [disabled]="editUserSubmitting">Cancelar</button>
|
<button type="button" class="btn-ghost" (click)="cancelEditUser()" [disabled]="editUserSubmitting">Cancelar</button>
|
||||||
<button type="submit" form="editUserHeaderForm" class="btn-primary" [disabled]="editUserSubmitting || !editUserTarget">
|
<button type="submit" form="editUserHeaderForm" class="btn-primary" [disabled]="editUserSubmitting || !editUserTarget">
|
||||||
|
|
@ -476,8 +467,8 @@
|
||||||
<div class="placeholder-icon">
|
<div class="placeholder-icon">
|
||||||
<i class="bi bi-person-gear"></i>
|
<i class="bi bi-person-gear"></i>
|
||||||
</div>
|
</div>
|
||||||
<h3>{{ isManageClientsMode ? 'Editar Credencial' : 'Editar Usuário' }}</h3>
|
<h3>Editar Usuário</h3>
|
||||||
<p>{{ isManageClientsMode ? 'Selecione uma credencial de cliente para visualizar e editar os detalhes.' : 'Selecione um usuário na lista para visualizar e editar os detalhes.' }}</p>
|
<p>Selecione um usuário na lista para visualizar e editar os detalhes.</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -547,7 +538,7 @@
|
||||||
<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="isSysAdmin" routerLink="/system/fornecer-usuario" routerLinkActive="active" class="side-item" (click)="closeMenu()">
|
<a *ngIf="isSysAdmin" routerLink="/system/fornecer-usuario" routerLinkActive="active" class="side-item" (click)="closeMenu()">
|
||||||
<i class="bi bi-shield-lock-fill"></i> <span>Criar credenciais do cliente</span>
|
<i class="bi bi-shield-lock-fill"></i> <span>Fornecer usuário</span>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</aside>
|
</aside>
|
||||||
|
|
|
||||||
|
|
@ -62,7 +62,6 @@ export class Header implements AfterViewInit, OnDestroy {
|
||||||
manageUsersErrors: ApiFieldError[] = [];
|
manageUsersErrors: ApiFieldError[] = [];
|
||||||
manageUsersSuccess = '';
|
manageUsersSuccess = '';
|
||||||
private manageUsersFeedbackTimer: ReturnType<typeof setTimeout> | null = null;
|
private manageUsersFeedbackTimer: ReturnType<typeof setTimeout> | null = null;
|
||||||
manageMode: 'users' | 'clients' = 'users';
|
|
||||||
manageUsers: any[] = [];
|
manageUsers: any[] = [];
|
||||||
manageSearch = '';
|
manageSearch = '';
|
||||||
managePage = 1;
|
managePage = 1;
|
||||||
|
|
@ -255,16 +254,6 @@ export class Header implements AfterViewInit, OnDestroy {
|
||||||
|
|
||||||
openManageUsersModal() {
|
openManageUsersModal() {
|
||||||
if (!this.isSysAdmin) return;
|
if (!this.isSysAdmin) return;
|
||||||
this.manageMode = 'users';
|
|
||||||
this.manageUsersOpen = true;
|
|
||||||
this.closeOptions();
|
|
||||||
this.resetManageUsersState();
|
|
||||||
this.fetchManageUsers(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
openManageClientCredentialsModal() {
|
|
||||||
if (!this.isSysAdmin) return;
|
|
||||||
this.manageMode = 'clients';
|
|
||||||
this.manageUsersOpen = true;
|
this.manageUsersOpen = true;
|
||||||
this.closeOptions();
|
this.closeOptions();
|
||||||
this.resetManageUsersState();
|
this.resetManageUsersState();
|
||||||
|
|
@ -274,7 +263,6 @@ export class Header implements AfterViewInit, OnDestroy {
|
||||||
closeManageUsersModal() {
|
closeManageUsersModal() {
|
||||||
this.manageUsersOpen = false;
|
this.manageUsersOpen = false;
|
||||||
this.resetManageUsersState();
|
this.resetManageUsersState();
|
||||||
this.manageMode = 'users';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
toggleNotifications() {
|
toggleNotifications() {
|
||||||
|
|
@ -672,7 +660,6 @@ export class Header implements AfterViewInit, OnDestroy {
|
||||||
this.usersService
|
this.usersService
|
||||||
.list({
|
.list({
|
||||||
search: this.manageSearch?.trim() || undefined,
|
search: this.manageSearch?.trim() || undefined,
|
||||||
permissao: this.isManageClientsMode ? 'cliente' : undefined,
|
|
||||||
page: this.managePage,
|
page: this.managePage,
|
||||||
pageSize: this.managePageSize,
|
pageSize: this.managePageSize,
|
||||||
})
|
})
|
||||||
|
|
@ -733,23 +720,17 @@ export class Header implements AfterViewInit, OnDestroy {
|
||||||
this.usersService.getById(user.id).subscribe({
|
this.usersService.getById(user.id).subscribe({
|
||||||
next: (full) => {
|
next: (full) => {
|
||||||
this.editUserTarget = full;
|
this.editUserTarget = full;
|
||||||
const permissao = this.isManageClientsMode ? 'cliente' : (full.permissao ?? '');
|
|
||||||
this.editUserForm.reset({
|
this.editUserForm.reset({
|
||||||
nome: full.nome ?? '',
|
nome: full.nome ?? '',
|
||||||
email: full.email ?? '',
|
email: full.email ?? '',
|
||||||
senha: '',
|
senha: '',
|
||||||
confirmarSenha: '',
|
confirmarSenha: '',
|
||||||
permissao,
|
permissao: full.permissao ?? '',
|
||||||
ativo: full.ativo ?? true,
|
ativo: full.ativo ?? true,
|
||||||
});
|
});
|
||||||
if (this.isManageClientsMode) {
|
|
||||||
this.editUserForm.get('permissao')?.disable({ emitEvent: false });
|
|
||||||
} else {
|
|
||||||
this.editUserForm.get('permissao')?.enable({ emitEvent: false });
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
error: () => {
|
error: () => {
|
||||||
this.editUserErrors = [{ message: this.isManageClientsMode ? 'Erro ao carregar credencial do cliente.' : 'Erro ao carregar usuário.' }];
|
this.editUserErrors = [{ message: 'Erro ao carregar usuario.' }];
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
@ -769,21 +750,12 @@ export class Header implements AfterViewInit, OnDestroy {
|
||||||
const payload: any = {};
|
const payload: any = {};
|
||||||
const nome = (this.editUserForm.get('nome')?.value || '').toString().trim();
|
const nome = (this.editUserForm.get('nome')?.value || '').toString().trim();
|
||||||
const email = (this.editUserForm.get('email')?.value || '').toString().trim();
|
const email = (this.editUserForm.get('email')?.value || '').toString().trim();
|
||||||
const permissao = this.isManageClientsMode
|
const permissao = (this.editUserForm.get('permissao')?.value || '').toString().trim();
|
||||||
? 'cliente'
|
|
||||||
: (this.editUserForm.get('permissao')?.value || '').toString().trim();
|
|
||||||
const ativo = !!this.editUserForm.get('ativo')?.value;
|
const ativo = !!this.editUserForm.get('ativo')?.value;
|
||||||
|
|
||||||
if (nome && nome !== (this.editUserTarget.nome || '').trim()) payload.nome = nome;
|
if (nome && nome !== (this.editUserTarget.nome || '').trim()) payload.nome = nome;
|
||||||
if (email && email !== (this.editUserTarget.email || '').trim()) payload.email = email;
|
if (email && email !== (this.editUserTarget.email || '').trim()) payload.email = email;
|
||||||
if (this.isManageClientsMode) {
|
if (permissao && permissao !== (this.editUserTarget.permissao || '').trim()) payload.permissao = permissao;
|
||||||
const targetPermissao = String(this.editUserTarget.permissao || '').trim().toLowerCase();
|
|
||||||
if (targetPermissao !== 'cliente') {
|
|
||||||
payload.permissao = 'cliente';
|
|
||||||
}
|
|
||||||
} else if (permissao && permissao !== (this.editUserTarget.permissao || '').trim()) {
|
|
||||||
payload.permissao = permissao;
|
|
||||||
}
|
|
||||||
if ((this.editUserTarget.ativo ?? true) !== ativo) payload.ativo = ativo;
|
if ((this.editUserTarget.ativo ?? true) !== ativo) payload.ativo = ativo;
|
||||||
|
|
||||||
const senha = (this.editUserForm.get('senha')?.value || '').toString();
|
const senha = (this.editUserForm.get('senha')?.value || '').toString();
|
||||||
|
|
@ -822,25 +794,18 @@ export class Header implements AfterViewInit, OnDestroy {
|
||||||
const merged = this.mergeUserUpdate(currentTarget, payload);
|
const merged = this.mergeUserUpdate(currentTarget, payload);
|
||||||
this.editUserSubmitting = false;
|
this.editUserSubmitting = false;
|
||||||
this.setEditFormDisabled(false);
|
this.setEditFormDisabled(false);
|
||||||
this.editUserSuccess = this.isManageClientsMode
|
this.editUserSuccess = `Usuario ${merged.nome} atualizado com sucesso.`;
|
||||||
? `Credencial de ${merged.nome} atualizada com sucesso.`
|
|
||||||
: `Usuario ${merged.nome} atualizado com sucesso.`;
|
|
||||||
this.editUserTarget = merged;
|
this.editUserTarget = merged;
|
||||||
this.editUserForm.patchValue({
|
this.editUserForm.patchValue({
|
||||||
nome: merged.nome ?? '',
|
nome: merged.nome ?? '',
|
||||||
email: merged.email ?? '',
|
email: merged.email ?? '',
|
||||||
permissao: this.isManageClientsMode ? 'cliente' : (merged.permissao ?? ''),
|
permissao: merged.permissao ?? '',
|
||||||
ativo: merged.ativo ?? true,
|
ativo: merged.ativo ?? true,
|
||||||
senha: '',
|
senha: '',
|
||||||
confirmarSenha: '',
|
confirmarSenha: '',
|
||||||
});
|
});
|
||||||
this.upsertManageUser(merged);
|
this.upsertManageUser(merged);
|
||||||
this.showManageUsersFeedback(
|
this.showManageUsersFeedback(`Usuario ${merged.nome} atualizado com sucesso.`, 'success');
|
||||||
this.isManageClientsMode
|
|
||||||
? `Credencial de ${merged.nome} atualizada com sucesso.`
|
|
||||||
: `Usuario ${merged.nome} atualizado com sucesso.`,
|
|
||||||
'success'
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
error: (err: HttpErrorResponse) => {
|
error: (err: HttpErrorResponse) => {
|
||||||
this.editUserSubmitting = false;
|
this.editUserSubmitting = false;
|
||||||
|
|
@ -849,17 +814,12 @@ export class Header implements AfterViewInit, OnDestroy {
|
||||||
if (Array.isArray(apiErrors)) {
|
if (Array.isArray(apiErrors)) {
|
||||||
this.editUserErrors = apiErrors.map((e: any) => ({
|
this.editUserErrors = apiErrors.map((e: any) => ({
|
||||||
field: e?.field,
|
field: e?.field,
|
||||||
message: e?.message || (this.isManageClientsMode ? 'Erro ao atualizar credencial do cliente.' : 'Erro ao atualizar usuario.'),
|
message: e?.message || 'Erro ao atualizar usuario.',
|
||||||
}));
|
}));
|
||||||
} else {
|
} else {
|
||||||
this.editUserErrors = [{
|
this.editUserErrors = [{ message: err?.error?.message || 'Erro ao atualizar usuario.' }];
|
||||||
message: err?.error?.message || (this.isManageClientsMode ? 'Erro ao atualizar credencial do cliente.' : 'Erro ao atualizar usuario.')
|
|
||||||
}];
|
|
||||||
}
|
}
|
||||||
this.showManageUsersFeedback(
|
this.showManageUsersFeedback(this.editUserErrors[0]?.message || 'Erro ao atualizar usuario.', 'error');
|
||||||
this.editUserErrors[0]?.message || (this.isManageClientsMode ? 'Erro ao atualizar credencial do cliente.' : 'Erro ao atualizar usuario.'),
|
|
||||||
'error'
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
@ -867,13 +827,11 @@ export class Header implements AfterViewInit, OnDestroy {
|
||||||
async confirmToggleUserStatus(user: any) {
|
async confirmToggleUserStatus(user: any) {
|
||||||
const nextActive = user.ativo === false;
|
const nextActive = user.ativo === false;
|
||||||
const actionLabel = nextActive ? 'reativar' : 'inativar';
|
const actionLabel = nextActive ? 'reativar' : 'inativar';
|
||||||
const entity = this.isManageClientsMode ? 'Credencial do Cliente' : 'Usuário';
|
|
||||||
const entityLower = this.isManageClientsMode ? 'credencial do cliente' : 'usuário';
|
|
||||||
const confirmed = await confirmActionModal({
|
const confirmed = await confirmActionModal({
|
||||||
title: nextActive ? `Reativar ${entity}` : `Inativar ${entity}`,
|
title: nextActive ? 'Reativar Usuário' : 'Inativar Usuário',
|
||||||
message: nextActive
|
message: nextActive
|
||||||
? `Deseja reativar ${entityLower} ${user.nome}? O acesso ao sistema será liberado novamente.`
|
? `Deseja reativar o usuário ${user.nome}? Ele voltará a ter acesso ao sistema.`
|
||||||
: `Deseja inativar ${entityLower} ${user.nome}? O acesso ao sistema ficará bloqueado até reativação.`,
|
: `Deseja inativar o usuário ${user.nome}? A conta ficará sem acesso até ser reativada.`,
|
||||||
confirmLabel: nextActive ? 'Reativar' : 'Inativar',
|
confirmLabel: nextActive ? 'Reativar' : 'Inativar',
|
||||||
tone: nextActive ? 'neutral' : 'warning',
|
tone: nextActive ? 'neutral' : 'warning',
|
||||||
});
|
});
|
||||||
|
|
@ -887,13 +845,11 @@ export class Header implements AfterViewInit, OnDestroy {
|
||||||
this.editUserTarget = { ...this.editUserTarget, ativo: nextActive };
|
this.editUserTarget = { ...this.editUserTarget, ativo: nextActive };
|
||||||
this.editUserForm.patchValue({ ativo: nextActive, senha: '', confirmarSenha: '' });
|
this.editUserForm.patchValue({ ativo: nextActive, senha: '', confirmarSenha: '' });
|
||||||
this.editUserErrors = [];
|
this.editUserErrors = [];
|
||||||
this.editUserSuccess = this.isManageClientsMode
|
this.editUserSuccess = `Usuario ${user.nome} ${nextActive ? 'reativado' : 'inativado'} com sucesso.`;
|
||||||
? `Credencial de ${user.nome} ${nextActive ? 'reativada' : 'inativada'} com sucesso.`
|
|
||||||
: `Usuario ${user.nome} ${nextActive ? 'reativado' : 'inativado'} com sucesso.`;
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
error: (err: HttpErrorResponse) => {
|
error: (err: HttpErrorResponse) => {
|
||||||
const message = err?.error?.message || `Erro ao ${actionLabel} ${this.isManageClientsMode ? 'credencial do cliente' : 'usuario'}.`;
|
const message = err?.error?.message || `Erro ao ${actionLabel} usuario.`;
|
||||||
if (this.editUserTarget?.id === user.id) {
|
if (this.editUserTarget?.id === user.id) {
|
||||||
this.editUserSuccess = '';
|
this.editUserSuccess = '';
|
||||||
this.editUserErrors = [{ message }];
|
this.editUserErrors = [{ message }];
|
||||||
|
|
@ -904,9 +860,7 @@ export class Header implements AfterViewInit, OnDestroy {
|
||||||
|
|
||||||
async confirmPermanentDeleteUser(user: any) {
|
async confirmPermanentDeleteUser(user: any) {
|
||||||
if (user?.ativo !== false) {
|
if (user?.ativo !== false) {
|
||||||
const message = this.isManageClientsMode
|
const message = 'Inative a conta antes de excluir permanentemente.';
|
||||||
? 'Inative a credencial antes de excluir permanentemente.'
|
|
||||||
: 'Inative a conta antes de excluir permanentemente.';
|
|
||||||
if (this.editUserTarget?.id === user?.id) {
|
if (this.editUserTarget?.id === user?.id) {
|
||||||
this.editUserSuccess = '';
|
this.editUserSuccess = '';
|
||||||
this.editUserErrors = [{ message }];
|
this.editUserErrors = [{ message }];
|
||||||
|
|
@ -916,9 +870,7 @@ export class Header implements AfterViewInit, OnDestroy {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const confirmed = await confirmDeletionWithTyping(
|
const confirmed = await confirmDeletionWithTyping(`o usuário ${user.nome}`);
|
||||||
this.isManageClientsMode ? `a credencial do cliente ${user.nome}` : `o usuário ${user.nome}`
|
|
||||||
);
|
|
||||||
if (!confirmed) return;
|
if (!confirmed) return;
|
||||||
|
|
||||||
this.usersService.delete(user.id).subscribe({
|
this.usersService.delete(user.id).subscribe({
|
||||||
|
|
@ -931,8 +883,8 @@ export class Header implements AfterViewInit, OnDestroy {
|
||||||
error: (err: HttpErrorResponse) => {
|
error: (err: HttpErrorResponse) => {
|
||||||
const apiErrors = err?.error?.errors;
|
const apiErrors = err?.error?.errors;
|
||||||
const message = Array.isArray(apiErrors)
|
const message = Array.isArray(apiErrors)
|
||||||
? (apiErrors[0]?.message || (this.isManageClientsMode ? 'Erro ao excluir credencial do cliente.' : 'Erro ao excluir usuario.'))
|
? (apiErrors[0]?.message || 'Erro ao excluir usuario.')
|
||||||
: (err?.error?.message || (this.isManageClientsMode ? 'Erro ao excluir credencial do cliente.' : 'Erro ao excluir usuario.'));
|
: (err?.error?.message || 'Erro ao excluir usuario.');
|
||||||
|
|
||||||
if (this.editUserTarget?.id === user.id) {
|
if (this.editUserTarget?.id === user.id) {
|
||||||
this.editUserSuccess = '';
|
this.editUserSuccess = '';
|
||||||
|
|
@ -984,30 +936,6 @@ export class Header implements AfterViewInit, OnDestroy {
|
||||||
this.cancelEditUser();
|
this.cancelEditUser();
|
||||||
}
|
}
|
||||||
|
|
||||||
get isManageClientsMode(): boolean {
|
|
||||||
return this.manageMode === 'clients';
|
|
||||||
}
|
|
||||||
|
|
||||||
get manageModalTitle(): string {
|
|
||||||
return this.isManageClientsMode ? 'Credenciais de Clientes' : 'Gestão de Usuários';
|
|
||||||
}
|
|
||||||
|
|
||||||
get manageListTitle(): string {
|
|
||||||
return this.isManageClientsMode ? 'Credenciais de Cliente' : 'Usuários';
|
|
||||||
}
|
|
||||||
|
|
||||||
get manageSearchPlaceholder(): string {
|
|
||||||
return this.isManageClientsMode
|
|
||||||
? 'Buscar por cliente, nome ou email...'
|
|
||||||
: 'Buscar por nome ou email...';
|
|
||||||
}
|
|
||||||
|
|
||||||
get editPermissionOptions() {
|
|
||||||
return this.isManageClientsMode
|
|
||||||
? [{ value: 'cliente', label: 'Cliente' }]
|
|
||||||
: this.permissionOptions;
|
|
||||||
}
|
|
||||||
|
|
||||||
private normalizeField(field?: string | null): string {
|
private normalizeField(field?: string | null): string {
|
||||||
return (field || '').trim().toLowerCase();
|
return (field || '').trim().toLowerCase();
|
||||||
}
|
}
|
||||||
|
|
@ -1019,12 +947,7 @@ export class Header implements AfterViewInit, OnDestroy {
|
||||||
|
|
||||||
private setEditFormDisabled(disabled: boolean) {
|
private setEditFormDisabled(disabled: boolean) {
|
||||||
if (disabled) this.editUserForm.disable({ emitEvent: false });
|
if (disabled) this.editUserForm.disable({ emitEvent: false });
|
||||||
else {
|
else this.editUserForm.enable({ emitEvent: false });
|
||||||
this.editUserForm.enable({ emitEvent: false });
|
|
||||||
if (this.isManageClientsMode) {
|
|
||||||
this.editUserForm.get('permissao')?.disable({ emitEvent: false });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private upsertManageUser(user: any) {
|
private upsertManageUser(user: any) {
|
||||||
|
|
|
||||||
|
|
@ -231,31 +231,34 @@
|
||||||
<div class="edit-sections">
|
<div class="edit-sections">
|
||||||
<details open class="detail-box">
|
<details open class="detail-box">
|
||||||
<summary class="box-header">
|
<summary class="box-header">
|
||||||
<span><i class="bi bi-link-45deg me-2"></i> Vínculo com Reserva</span>
|
<span><i class="bi bi-link-45deg me-2"></i> Vínculo com GERAL</span>
|
||||||
<i class="bi bi-chevron-down ms-auto transition-icon"></i>
|
<i class="bi bi-chevron-down ms-auto transition-icon"></i>
|
||||||
</summary>
|
</summary>
|
||||||
<div class="box-body">
|
<div class="box-body">
|
||||||
<div class="form-grid">
|
<div class="form-grid">
|
||||||
<div class="form-field span-2">
|
<div class="form-field span-2">
|
||||||
<label>Linha (RESERVA)</label>
|
<label>Cliente (GERAL)</label>
|
||||||
|
<app-select
|
||||||
|
class="form-select"
|
||||||
|
size="sm"
|
||||||
|
[options]="clientsFromGeral"
|
||||||
|
[(ngModel)]="createModel.selectedClient"
|
||||||
|
(ngModelChange)="onCreateClientChange()"
|
||||||
|
[disabled]="createClientsLoading"
|
||||||
|
></app-select>
|
||||||
|
</div>
|
||||||
|
<div class="form-field span-2">
|
||||||
|
<label>Linha (GERAL)</label>
|
||||||
<app-select
|
<app-select
|
||||||
class="form-select"
|
class="form-select"
|
||||||
size="sm"
|
size="sm"
|
||||||
[options]="lineOptionsCreate"
|
[options]="lineOptionsCreate"
|
||||||
labelKey="label"
|
labelKey="label"
|
||||||
valueKey="id"
|
valueKey="id"
|
||||||
[searchable]="true"
|
|
||||||
searchPlaceholder="Pesquisar linha da reserva..."
|
|
||||||
[(ngModel)]="createModel.mobileLineId"
|
[(ngModel)]="createModel.mobileLineId"
|
||||||
(ngModelChange)="onCreateLineChange()"
|
(ngModelChange)="onCreateLineChange()"
|
||||||
[disabled]="createLinesLoading"
|
[disabled]="createLinesLoading || !createModel.selectedClient"
|
||||||
placeholder="Selecione uma linha da Reserva..."
|
|
||||||
></app-select>
|
></app-select>
|
||||||
<small class="field-hint" *ngIf="createLinesLoading">Carregando linhas da Reserva...</small>
|
|
||||||
</div>
|
|
||||||
<div class="form-field">
|
|
||||||
<label>Total Franquia Line</label>
|
|
||||||
<input class="form-control form-control-sm bg-light" [value]="formatFranquiaLine(createFranquiaLineTotal)" readonly />
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -288,10 +291,7 @@
|
||||||
<label>Razão Social</label>
|
<label>Razão Social</label>
|
||||||
<input class="form-control form-control-sm" [(ngModel)]="createModel.razaoSocial" />
|
<input class="form-control form-control-sm" [(ngModel)]="createModel.razaoSocial" />
|
||||||
</div>
|
</div>
|
||||||
<div class="form-field field-line">
|
<div class="form-field field-line"><label>Linha</label><input class="form-control form-control-sm" inputmode="numeric" [(ngModel)]="createModel.linha" /></div>
|
||||||
<label>Linha</label>
|
|
||||||
<input class="form-control form-control-sm bg-light" [value]="createModel.linha || ''" readonly />
|
|
||||||
</div>
|
|
||||||
<div class="form-field field-item field-auto">
|
<div class="form-field field-item field-auto">
|
||||||
<label>Item (Automático)</label>
|
<label>Item (Automático)</label>
|
||||||
<input class="form-control form-control-sm bg-light" type="number" [(ngModel)]="createModel.item" readonly title="Gerado automaticamente pelo sistema" />
|
<input class="form-control form-control-sm bg-light" type="number" [(ngModel)]="createModel.item" readonly title="Gerado automaticamente pelo sistema" />
|
||||||
|
|
@ -357,26 +357,7 @@
|
||||||
<label>Razão Social</label>
|
<label>Razão Social</label>
|
||||||
<input class="form-control form-control-sm" [(ngModel)]="editModel.razaoSocial" />
|
<input class="form-control form-control-sm" [(ngModel)]="editModel.razaoSocial" />
|
||||||
</div>
|
</div>
|
||||||
<div class="form-field field-line">
|
<div class="form-field field-line"><label>Linha</label><input class="form-control form-control-sm" inputmode="numeric" [(ngModel)]="editModel.linha" /></div>
|
||||||
<label>Linha (Reserva)</label>
|
|
||||||
<app-select
|
|
||||||
class="form-select"
|
|
||||||
size="sm"
|
|
||||||
[options]="editLineOptions"
|
|
||||||
labelKey="label"
|
|
||||||
valueKey="id"
|
|
||||||
[searchable]="true"
|
|
||||||
searchPlaceholder="Pesquisar linha da reserva..."
|
|
||||||
[(ngModel)]="editSelectedLineId"
|
|
||||||
(ngModelChange)="onEditLineChange()"
|
|
||||||
[disabled]="createLinesLoading"
|
|
||||||
placeholder="Selecione uma linha da Reserva..."
|
|
||||||
></app-select>
|
|
||||||
</div>
|
|
||||||
<div class="form-field field-auto">
|
|
||||||
<label>Total Franquia Line</label>
|
|
||||||
<input class="form-control form-control-sm bg-light" [value]="formatFranquiaLine(editFranquiaLineTotal)" readonly />
|
|
||||||
</div>
|
|
||||||
<div class="form-field field-item field-auto">
|
<div class="form-field field-item field-auto">
|
||||||
<label>Item (Automático)</label>
|
<label>Item (Automático)</label>
|
||||||
<input class="form-control form-control-sm bg-light" type="number" [(ngModel)]="editModel.item" readonly title="Gerado automaticamente pelo sistema" />
|
<input class="form-control form-control-sm bg-light" type="number" [(ngModel)]="editModel.item" readonly title="Gerado automaticamente pelo sistema" />
|
||||||
|
|
|
||||||
|
|
@ -24,7 +24,6 @@ interface LineOptionDto {
|
||||||
item: number;
|
item: number;
|
||||||
linha: string | null;
|
linha: string | null;
|
||||||
usuario: string | null;
|
usuario: string | null;
|
||||||
franquiaLine?: number | null;
|
|
||||||
label?: string;
|
label?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -95,15 +94,13 @@ export class DadosUsuarios implements OnInit {
|
||||||
createSaving = false;
|
createSaving = false;
|
||||||
createModel: any = null;
|
createModel: any = null;
|
||||||
createDateNascimento = '';
|
createDateNascimento = '';
|
||||||
createFranquiaLineTotal = 0;
|
clientsFromGeral: string[] = [];
|
||||||
editFranquiaLineTotal = 0;
|
|
||||||
editSelectedLineId = '';
|
|
||||||
editLineOptions: LineOptionDto[] = [];
|
|
||||||
lineOptionsCreate: LineOptionDto[] = [];
|
lineOptionsCreate: LineOptionDto[] = [];
|
||||||
readonly tipoPessoaOptions: SimpleOption[] = [
|
readonly tipoPessoaOptions: SimpleOption[] = [
|
||||||
{ label: 'Pessoa Física', value: 'PF' },
|
{ label: 'Pessoa Física', value: 'PF' },
|
||||||
{ label: 'Pessoa Jurídica', value: 'PJ' },
|
{ label: 'Pessoa Jurídica', value: 'PJ' },
|
||||||
];
|
];
|
||||||
|
createClientsLoading = false;
|
||||||
createLinesLoading = false;
|
createLinesLoading = false;
|
||||||
|
|
||||||
isSysAdmin = false;
|
isSysAdmin = false;
|
||||||
|
|
@ -298,11 +295,7 @@ export class DadosUsuarios implements OnInit {
|
||||||
razaoSocial: fullData.razaoSocial || (tipo === 'PJ' ? fullData.cliente : '')
|
razaoSocial: fullData.razaoSocial || (tipo === 'PJ' ? fullData.cliente : '')
|
||||||
};
|
};
|
||||||
this.editDateNascimento = this.toDateInput(fullData.dataNascimento);
|
this.editDateNascimento = this.toDateInput(fullData.dataNascimento);
|
||||||
this.editFranquiaLineTotal = 0;
|
|
||||||
this.editSelectedLineId = '';
|
|
||||||
this.editLineOptions = [];
|
|
||||||
this.editOpen = true;
|
this.editOpen = true;
|
||||||
this.loadReserveLinesForSelects();
|
|
||||||
},
|
},
|
||||||
error: () => this.showToast('Erro ao abrir edição', 'danger')
|
error: () => this.showToast('Erro ao abrir edição', 'danger')
|
||||||
});
|
});
|
||||||
|
|
@ -314,9 +307,6 @@ export class DadosUsuarios implements OnInit {
|
||||||
this.editModel = null;
|
this.editModel = null;
|
||||||
this.editDateNascimento = '';
|
this.editDateNascimento = '';
|
||||||
this.editingId = null;
|
this.editingId = null;
|
||||||
this.editSelectedLineId = '';
|
|
||||||
this.editLineOptions = [];
|
|
||||||
this.editFranquiaLineTotal = 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onEditTipoChange() {
|
onEditTipoChange() {
|
||||||
|
|
@ -379,14 +369,13 @@ export class DadosUsuarios implements OnInit {
|
||||||
if (!this.isSysAdmin) return;
|
if (!this.isSysAdmin) return;
|
||||||
this.resetCreateModel();
|
this.resetCreateModel();
|
||||||
this.createOpen = true;
|
this.createOpen = true;
|
||||||
this.loadReserveLinesForSelects();
|
this.preloadGeralClients();
|
||||||
}
|
}
|
||||||
|
|
||||||
closeCreate() {
|
closeCreate() {
|
||||||
this.createOpen = false;
|
this.createOpen = false;
|
||||||
this.createSaving = false;
|
this.createSaving = false;
|
||||||
this.createModel = null;
|
this.createModel = null;
|
||||||
this.createFranquiaLineTotal = 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private resetCreateModel() {
|
private resetCreateModel() {
|
||||||
|
|
@ -408,9 +397,33 @@ export class DadosUsuarios implements OnInit {
|
||||||
telefoneFixo: ''
|
telefoneFixo: ''
|
||||||
};
|
};
|
||||||
this.createDateNascimento = '';
|
this.createDateNascimento = '';
|
||||||
this.createFranquiaLineTotal = 0;
|
|
||||||
this.lineOptionsCreate = [];
|
this.lineOptionsCreate = [];
|
||||||
this.createLinesLoading = false;
|
this.createLinesLoading = false;
|
||||||
|
this.createClientsLoading = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private preloadGeralClients() {
|
||||||
|
this.createClientsLoading = true;
|
||||||
|
this.linesService.getClients().subscribe({
|
||||||
|
next: (list) => {
|
||||||
|
this.clientsFromGeral = list ?? [];
|
||||||
|
this.createClientsLoading = false;
|
||||||
|
},
|
||||||
|
error: () => {
|
||||||
|
this.clientsFromGeral = [];
|
||||||
|
this.createClientsLoading = false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
onCreateClientChange() {
|
||||||
|
const c = (this.createModel?.selectedClient ?? '').trim();
|
||||||
|
this.createModel.mobileLineId = '';
|
||||||
|
this.createModel.linha = '';
|
||||||
|
this.createModel.cliente = c;
|
||||||
|
this.lineOptionsCreate = [];
|
||||||
|
|
||||||
|
if (c) this.loadLinesForClient(c);
|
||||||
}
|
}
|
||||||
|
|
||||||
onCreateTipoChange() {
|
onCreateTipoChange() {
|
||||||
|
|
@ -425,9 +438,12 @@ export class DadosUsuarios implements OnInit {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private loadReserveLinesForSelects(onDone?: () => void) {
|
private loadLinesForClient(cliente: string) {
|
||||||
|
const c = (cliente ?? '').trim();
|
||||||
|
if (!c) return;
|
||||||
|
|
||||||
this.createLinesLoading = true;
|
this.createLinesLoading = true;
|
||||||
this.linesService.getLinesByClient('RESERVA').subscribe({
|
this.linesService.getLinesByClient(c).subscribe({
|
||||||
next: (items: any[]) => {
|
next: (items: any[]) => {
|
||||||
const mapped: LineOptionDto[] = (items ?? [])
|
const mapped: LineOptionDto[] = (items ?? [])
|
||||||
.filter(x => !!String(x?.id ?? '').trim())
|
.filter(x => !!String(x?.id ?? '').trim())
|
||||||
|
|
@ -441,16 +457,12 @@ export class DadosUsuarios implements OnInit {
|
||||||
.filter(x => !!String(x.linha ?? '').trim());
|
.filter(x => !!String(x.linha ?? '').trim());
|
||||||
|
|
||||||
this.lineOptionsCreate = mapped;
|
this.lineOptionsCreate = mapped;
|
||||||
if (this.editModel) this.syncEditLineOptions();
|
|
||||||
this.createLinesLoading = false;
|
this.createLinesLoading = false;
|
||||||
onDone?.();
|
|
||||||
},
|
},
|
||||||
error: () => {
|
error: () => {
|
||||||
this.lineOptionsCreate = [];
|
this.lineOptionsCreate = [];
|
||||||
this.editLineOptions = [];
|
|
||||||
this.createLinesLoading = false;
|
this.createLinesLoading = false;
|
||||||
this.showToast('Erro ao carregar linhas da Reserva.', 'danger');
|
this.showToast('Erro ao carregar linhas da GERAL.', 'danger');
|
||||||
onDone?.();
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
@ -465,56 +477,13 @@ export class DadosUsuarios implements OnInit {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
onEditLineChange() {
|
|
||||||
const id = String(this.editSelectedLineId ?? '').trim();
|
|
||||||
if (!id || id === '__CURRENT__') return;
|
|
||||||
this.linesService.getById(id).subscribe({
|
|
||||||
next: (d: MobileLineDetail) => this.applyLineDetailToEdit(d),
|
|
||||||
error: () => this.showToast('Erro ao carregar dados da linha.', 'danger')
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private syncEditLineOptions() {
|
|
||||||
if (!this.editModel) {
|
|
||||||
this.editLineOptions = [];
|
|
||||||
this.editSelectedLineId = '';
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const currentLine = String(this.editModel.linha ?? '').trim();
|
|
||||||
const fromReserva = this.lineOptionsCreate.find((x) => String(x.linha ?? '').trim() === currentLine);
|
|
||||||
const options = [...this.lineOptionsCreate];
|
|
||||||
|
|
||||||
if (currentLine && !fromReserva) {
|
|
||||||
options.unshift({
|
|
||||||
id: '__CURRENT__',
|
|
||||||
item: Number(this.editModel.item ?? 0),
|
|
||||||
linha: currentLine,
|
|
||||||
usuario: this.editModel.cliente ?? null,
|
|
||||||
label: `Atual • ${currentLine}`
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
this.editLineOptions = options;
|
|
||||||
this.editSelectedLineId = fromReserva?.id ?? (currentLine ? '__CURRENT__' : '');
|
|
||||||
if (fromReserva?.id) {
|
|
||||||
this.onEditLineChange();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private applyLineDetailToCreate(d: MobileLineDetail) {
|
private applyLineDetailToCreate(d: MobileLineDetail) {
|
||||||
this.createModel.linha = d.linha ?? '';
|
this.createModel.linha = d.linha ?? '';
|
||||||
this.createFranquiaLineTotal = this.toNullableNumber(d.franquiaLine) ?? 0;
|
this.createModel.cliente = d.cliente ?? this.createModel.cliente ?? '';
|
||||||
if (!String(this.createModel.item ?? '').trim() && d.item) {
|
if (!String(this.createModel.item ?? '').trim() && d.item) {
|
||||||
this.createModel.item = String(d.item);
|
this.createModel.item = String(d.item);
|
||||||
}
|
}
|
||||||
|
|
||||||
const lineClient = String(d.cliente ?? '').trim();
|
|
||||||
const isReserva = lineClient.localeCompare('RESERVA', 'pt-BR', { sensitivity: 'base' }) === 0;
|
|
||||||
if (!isReserva && lineClient) {
|
|
||||||
this.createModel.cliente = lineClient;
|
|
||||||
}
|
|
||||||
|
|
||||||
if ((this.createModel.tipoPessoa ?? '').toUpperCase() === 'PJ') {
|
if ((this.createModel.tipoPessoa ?? '').toUpperCase() === 'PJ') {
|
||||||
if (!this.createModel.razaoSocial) this.createModel.razaoSocial = this.createModel.cliente;
|
if (!this.createModel.razaoSocial) this.createModel.razaoSocial = this.createModel.cliente;
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -522,15 +491,6 @@ export class DadosUsuarios implements OnInit {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private applyLineDetailToEdit(d: MobileLineDetail) {
|
|
||||||
if (!this.editModel) return;
|
|
||||||
this.editModel.linha = d.linha ?? this.editModel.linha;
|
|
||||||
this.editFranquiaLineTotal = this.toNullableNumber(d.franquiaLine) ?? 0;
|
|
||||||
if (!String(this.editModel.item ?? '').trim() && d.item) {
|
|
||||||
this.editModel.item = d.item;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
saveCreate() {
|
saveCreate() {
|
||||||
if (!this.createModel) return;
|
if (!this.createModel) return;
|
||||||
this.createSaving = true;
|
this.createSaving = true;
|
||||||
|
|
@ -624,11 +584,6 @@ export class DadosUsuarios implements OnInit {
|
||||||
return Number.isNaN(n) ? null : n;
|
return Number.isNaN(n) ? null : n;
|
||||||
}
|
}
|
||||||
|
|
||||||
formatFranquiaLine(value: any): string {
|
|
||||||
const n = this.toNullableNumber(value) ?? 0;
|
|
||||||
return `${n.toLocaleString('pt-BR', { minimumFractionDigits: 0, maximumFractionDigits: 2 })} GB`;
|
|
||||||
}
|
|
||||||
|
|
||||||
private normalizeTipo(row: UserDataRow | null | undefined): 'PF' | 'PJ' {
|
private normalizeTipo(row: UserDataRow | null | undefined): 'PF' | 'PJ' {
|
||||||
const t = (row?.tipoPessoa ?? '').toString().trim().toUpperCase();
|
const t = (row?.tipoPessoa ?? '').toString().trim().toUpperCase();
|
||||||
if (t === 'PJ') return 'PJ';
|
if (t === 'PJ') return 'PJ';
|
||||||
|
|
|
||||||
|
|
@ -7,12 +7,10 @@
|
||||||
<div class="page-head fade-in-up">
|
<div class="page-head fade-in-up">
|
||||||
<div class="head-content">
|
<div class="head-content">
|
||||||
<div class="badge-pill">
|
<div class="badge-pill">
|
||||||
<i class="bi bi-grid-1x2-fill"></i> {{ isCliente ? 'Visão Cliente' : 'Visão Geral' }}
|
<i class="bi bi-grid-1x2-fill"></i> Visão Geral
|
||||||
</div>
|
</div>
|
||||||
<h1 class="page-title">{{ isCliente ? 'Dashboard do Cliente' : 'Dashboard de Gestão de Linhas' }}</h1>
|
<h1 class="page-title">Dashboard de Gestão de Linhas</h1>
|
||||||
<p class="page-subtitle">
|
<p class="page-subtitle">Painel operacional com foco em status, cobertura e histórico da base.</p>
|
||||||
{{ isCliente ? 'Acompanhe suas linhas em tempo real com foco em operação e disponibilidade.' : 'Painel operacional com foco em status, cobertura e histórico da base.' }}
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="head-actions">
|
<div class="head-actions">
|
||||||
|
|
@ -29,7 +27,7 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="hero-grid fade-in-up" [style.animation-delay]="'100ms'" *ngIf="!isCliente || clientOverview.hasData">
|
<div class="hero-grid fade-in-up" [style.animation-delay]="'100ms'">
|
||||||
<div class="hero-card" *ngFor="let k of kpis; trackBy: trackByKpiKey">
|
<div class="hero-card" *ngFor="let k of kpis; trackBy: trackByKpiKey">
|
||||||
<div class="hero-icon">
|
<div class="hero-icon">
|
||||||
<i [class]="k.icon"></i>
|
<i [class]="k.icon"></i>
|
||||||
|
|
@ -328,120 +326,45 @@
|
||||||
</ng-container>
|
</ng-container>
|
||||||
|
|
||||||
<ng-template #clienteDashboard>
|
<ng-template #clienteDashboard>
|
||||||
<ng-container *ngIf="clientOverview.hasData; else clienteSemDados">
|
<div class="dashboard-section fade-in-up" [style.animation-delay]="'180ms'">
|
||||||
<div class="context-title fade-in-up" [style.animation-delay]="'180ms'">
|
<div class="section-top-row">
|
||||||
<h2>Monitoramento da Sua Base</h2>
|
<div class="card-modern card-status">
|
||||||
<p>Visão operacional das suas linhas para acompanhar uso, status e disponibilidade.</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="dashboard-section fade-in-up" [style.animation-delay]="'220ms'">
|
|
||||||
<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 das Linhas</h3>
|
|
||||||
<p>Distribuição atual da sua base</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-soft"></span>
|
|
||||||
<span class="lbl">Demais Linhas</span>
|
|
||||||
<span class="val">{{ clientDemaisLinhas | 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 class="card-modern">
|
|
||||||
<div class="card-header-clean">
|
|
||||||
<div class="header-icon blue"><i class="bi bi-wifi"></i></div>
|
|
||||||
<div class="header-text">
|
|
||||||
<h3>Faixa de Franquia Line</h3>
|
|
||||||
<p>Quantidade de linhas por faixa de franquia contratada</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="chart-wrapper-bar compact-half">
|
|
||||||
<canvas #chartLinhasPorFranquia></canvas>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="dashboard-section fade-in-up" [style.animation-delay]="'260ms'">
|
|
||||||
<div class="card-modern full-width">
|
|
||||||
<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-text">
|
<div class="header-text">
|
||||||
<h3>Top Planos (Qtd. Linhas)</h3>
|
<h3>Status da Base</h3>
|
||||||
<p>Planos com maior volume na sua operação</p>
|
<p>Distribuição atual das linhas</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="chart-wrapper-bar compact">
|
<div class="card-body-split">
|
||||||
<canvas #chartResumoTopPlanos></canvas>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="grid-halves mt-3 client-secondary-grid">
|
|
||||||
<div class="card-modern">
|
|
||||||
<div class="card-header-clean">
|
|
||||||
<div class="header-text">
|
|
||||||
<h3>Top Usuários (Qtd. Linhas)</h3>
|
|
||||||
<p>Apenas usuários de fato (sem bloqueados/aguardando)</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="chart-wrapper-bar compact">
|
|
||||||
<canvas #chartResumoTopClientes></canvas>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="card-modern">
|
|
||||||
<div class="card-header-clean">
|
|
||||||
<div class="header-icon brand"><i class="bi bi-sim"></i></div>
|
|
||||||
<div class="header-text">
|
|
||||||
<h3>Tipo de Chip</h3>
|
|
||||||
<p>Distribuição entre e-SIM, SIMCARD e outros</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="chart-wrapper-pie">
|
<div class="chart-wrapper-pie">
|
||||||
<canvas #chartTipoChip></canvas>
|
<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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</ng-container>
|
</div>
|
||||||
|
|
||||||
<ng-template #clienteSemDados>
|
|
||||||
<div class="dashboard-section fade-in-up" [style.animation-delay]="'180ms'">
|
|
||||||
<div class="card-modern full-width">
|
|
||||||
<div class="card-header-clean">
|
|
||||||
<div class="header-icon warning"><i class="bi bi-info-circle-fill"></i></div>
|
|
||||||
<div class="header-text">
|
|
||||||
<h3>Sem dados para exibição</h3>
|
|
||||||
<p>Não encontramos linhas vinculadas ao seu acesso no momento.</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="card-body-grid">
|
|
||||||
<p class="mb-0 text-muted">
|
|
||||||
Assim que a base deste cliente estiver disponível na página Geral, os KPIs e gráficos serão atualizados automaticamente.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</ng-template>
|
|
||||||
</ng-template>
|
</ng-template>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
|
||||||
|
|
@ -163,10 +163,6 @@
|
||||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||||
gap: 16px;
|
gap: 16px;
|
||||||
margin-bottom: 32px;
|
margin-bottom: 32px;
|
||||||
|
|
||||||
@media (min-width: 1500px) {
|
|
||||||
grid-template-columns: repeat(6, minmax(0, 1fr));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.hero-card {
|
.hero-card {
|
||||||
|
|
|
||||||
|
|
@ -111,7 +111,6 @@ type InsightsChartSeries = {
|
||||||
type InsightsKpisVivo = {
|
type InsightsKpisVivo = {
|
||||||
qtdLinhas?: number | null;
|
qtdLinhas?: number | null;
|
||||||
totalFranquiaGb?: number | null;
|
totalFranquiaGb?: number | null;
|
||||||
totalFranquiaLine?: number | null;
|
|
||||||
totalBaseMensal?: number | null;
|
totalBaseMensal?: number | null;
|
||||||
totalAdicionaisMensal?: number | null;
|
totalAdicionaisMensal?: number | null;
|
||||||
totalGeralMensal?: number | null;
|
totalGeralMensal?: number | null;
|
||||||
|
|
@ -156,13 +155,6 @@ type DashboardGeralInsightsDto = {
|
||||||
};
|
};
|
||||||
|
|
||||||
type DashboardLineListItemDto = {
|
type DashboardLineListItemDto = {
|
||||||
linha?: string | null;
|
|
||||||
cliente?: string | null;
|
|
||||||
usuario?: string | null;
|
|
||||||
skil?: string | null;
|
|
||||||
planoContrato?: string | null;
|
|
||||||
status?: string | null;
|
|
||||||
franquiaLine?: number | null;
|
|
||||||
gestaoVozDados?: number | null;
|
gestaoVozDados?: number | null;
|
||||||
skeelo?: number | null;
|
skeelo?: number | null;
|
||||||
vivoNewsPlus?: number | null;
|
vivoNewsPlus?: number | null;
|
||||||
|
|
@ -172,6 +164,13 @@ type DashboardLineListItemDto = {
|
||||||
tipoDeChip?: string | null;
|
tipoDeChip?: string | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type DashboardLinesPageDto = {
|
||||||
|
page: number;
|
||||||
|
pageSize: number;
|
||||||
|
total: number;
|
||||||
|
items: DashboardLineListItemDto[];
|
||||||
|
};
|
||||||
|
|
||||||
type ResumoTopCliente = {
|
type ResumoTopCliente = {
|
||||||
cliente: string;
|
cliente: string;
|
||||||
linhas: number;
|
linhas: number;
|
||||||
|
|
@ -194,18 +193,6 @@ type ResumoDiferencaPjPf = {
|
||||||
totalLinhas: number | null;
|
totalLinhas: number | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
type ClientDashboardOverview = {
|
|
||||||
hasData: boolean;
|
|
||||||
totalLinhas: number;
|
|
||||||
ativas: number;
|
|
||||||
bloqueadas: number;
|
|
||||||
reservas: number;
|
|
||||||
franquiaLineTotalGb: number;
|
|
||||||
planosContratados: number;
|
|
||||||
usuariosComLinha: number;
|
|
||||||
outrosStatus: number;
|
|
||||||
};
|
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-dashboard',
|
selector: 'app-dashboard',
|
||||||
standalone: true,
|
standalone: true,
|
||||||
|
|
@ -299,17 +286,6 @@ export class Dashboard implements OnInit, AfterViewInit, OnDestroy {
|
||||||
resumo: ResumoResponse | null = null;
|
resumo: ResumoResponse | null = null;
|
||||||
resumoTopN = 5;
|
resumoTopN = 5;
|
||||||
resumoTopOptions = [5, 10, 15];
|
resumoTopOptions = [5, 10, 15];
|
||||||
clientOverview: ClientDashboardOverview = {
|
|
||||||
hasData: false,
|
|
||||||
totalLinhas: 0,
|
|
||||||
ativas: 0,
|
|
||||||
bloqueadas: 0,
|
|
||||||
reservas: 0,
|
|
||||||
franquiaLineTotalGb: 0,
|
|
||||||
planosContratados: 0,
|
|
||||||
usuariosComLinha: 0,
|
|
||||||
outrosStatus: 0,
|
|
||||||
};
|
|
||||||
|
|
||||||
// Resumo Derived Data
|
// Resumo Derived Data
|
||||||
resumoTopClientes: ResumoTopCliente[] = [];
|
resumoTopClientes: ResumoTopCliente[] = [];
|
||||||
|
|
@ -372,14 +348,11 @@ export class Dashboard implements OnInit, AfterViewInit, OnDestroy {
|
||||||
const isGestor = this.authService.hasRole('gestor');
|
const isGestor = this.authService.hasRole('gestor');
|
||||||
this.isCliente = !(isSysAdmin || isGestor);
|
this.isCliente = !(isSysAdmin || isGestor);
|
||||||
|
|
||||||
if (this.isCliente) {
|
|
||||||
this.loadClientDashboardData();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.loadDashboard();
|
this.loadDashboard();
|
||||||
this.loadInsights();
|
if (!this.isCliente) {
|
||||||
this.loadResumoExecutive();
|
this.loadInsights();
|
||||||
|
this.loadResumoExecutive();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ngAfterViewInit(): void {
|
ngAfterViewInit(): void {
|
||||||
|
|
@ -416,245 +389,6 @@ export class Dashboard implements OnInit, AfterViewInit, OnDestroy {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async loadClientDashboardData() {
|
|
||||||
this.loading = true;
|
|
||||||
this.errorMsg = null;
|
|
||||||
this.dataReady = false;
|
|
||||||
this.resumoLoading = true;
|
|
||||||
this.resumoError = null;
|
|
||||||
this.resumoReady = false;
|
|
||||||
|
|
||||||
try {
|
|
||||||
const [operacionais, reservas] = await Promise.all([
|
|
||||||
this.fetchAllDashboardLines(false),
|
|
||||||
this.fetchAllDashboardLines(true),
|
|
||||||
]);
|
|
||||||
const allLines = [...operacionais, ...reservas];
|
|
||||||
this.applyClientLineAggregates(allLines);
|
|
||||||
|
|
||||||
this.loading = false;
|
|
||||||
this.resumoLoading = false;
|
|
||||||
this.dataReady = true;
|
|
||||||
this.resumoReady = true;
|
|
||||||
this.tryBuildCharts();
|
|
||||||
this.tryBuildResumoCharts();
|
|
||||||
} catch (error) {
|
|
||||||
this.loading = false;
|
|
||||||
this.resumoLoading = false;
|
|
||||||
this.resumoReady = false;
|
|
||||||
this.dataReady = false;
|
|
||||||
this.errorMsg = this.isNetworkError(error)
|
|
||||||
? 'Falha ao carregar o Dashboard. Verifique a conexão.'
|
|
||||||
: 'Falha ao carregar os dados do cliente.';
|
|
||||||
this.clearClientDashboardState();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async fetchAllDashboardLines(onlyReserva: boolean): Promise<DashboardLineListItemDto[]> {
|
|
||||||
const pageSize = 500;
|
|
||||||
let page = 1;
|
|
||||||
const all: DashboardLineListItemDto[] = [];
|
|
||||||
|
|
||||||
while (true) {
|
|
||||||
let params = new HttpParams()
|
|
||||||
.set('page', String(page))
|
|
||||||
.set('pageSize', String(pageSize));
|
|
||||||
|
|
||||||
if (onlyReserva) {
|
|
||||||
params = params.set('skil', 'RESERVA');
|
|
||||||
}
|
|
||||||
|
|
||||||
const response = await firstValueFrom(this.http.get<any>(`${this.baseApi}/lines`, { params }));
|
|
||||||
const itemsRaw = this.readNode(response, 'items', 'Items');
|
|
||||||
const items = Array.isArray(itemsRaw) ? (itemsRaw as DashboardLineListItemDto[]) : [];
|
|
||||||
all.push(...items);
|
|
||||||
|
|
||||||
const total = this.toNumberOrNull(this.readNode(response, 'total', 'Total'));
|
|
||||||
if (!items.length) break;
|
|
||||||
if (total !== null && all.length >= total) break;
|
|
||||||
if (items.length < pageSize) break;
|
|
||||||
|
|
||||||
page += 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
return all;
|
|
||||||
}
|
|
||||||
|
|
||||||
private applyClientLineAggregates(
|
|
||||||
allLines: DashboardLineListItemDto[]
|
|
||||||
): void {
|
|
||||||
const planMap = new Map<string, number>();
|
|
||||||
const userMap = new Map<string, number>();
|
|
||||||
const franquiaBandMap = new Map<string, number>();
|
|
||||||
|
|
||||||
let totalLinhas = 0;
|
|
||||||
let ativas = 0;
|
|
||||||
let bloqueadas = 0;
|
|
||||||
let reservas = 0;
|
|
||||||
let outrosStatus = 0;
|
|
||||||
let franquiaLineTotalGb = 0;
|
|
||||||
let eSim = 0;
|
|
||||||
let simCard = 0;
|
|
||||||
let outrosChip = 0;
|
|
||||||
|
|
||||||
for (const line of allLines) {
|
|
||||||
totalLinhas += 1;
|
|
||||||
const isReserva = this.isReservaLine(line);
|
|
||||||
const status = this.normalizeSeriesKey(this.readLineString(line, 'status', 'Status'));
|
|
||||||
const planoContrato = this.readLineString(line, 'planoContrato', 'PlanoContrato').trim();
|
|
||||||
const usuario = this.readLineString(line, 'usuario', 'Usuario').trim();
|
|
||||||
const usuarioKey = this.normalizeSeriesKey(usuario);
|
|
||||||
const franquiaLine = this.readLineNumber(line, 'franquiaLine', 'FranquiaLine');
|
|
||||||
franquiaLineTotalGb += franquiaLine > 0 ? franquiaLine : 0;
|
|
||||||
|
|
||||||
if (isReserva) {
|
|
||||||
reservas += 1;
|
|
||||||
} else if (status.includes('ATIV')) {
|
|
||||||
ativas += 1;
|
|
||||||
} else if (
|
|
||||||
status.includes('BLOQUE') ||
|
|
||||||
status.includes('PERDA') ||
|
|
||||||
status.includes('ROUBO') ||
|
|
||||||
status.includes('SUSPEN') ||
|
|
||||||
status.includes('CANCEL')
|
|
||||||
) {
|
|
||||||
bloqueadas += 1;
|
|
||||||
} else {
|
|
||||||
outrosStatus += 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!isReserva) {
|
|
||||||
const planoKey = planoContrato || 'Sem plano';
|
|
||||||
planMap.set(planoKey, (planMap.get(planoKey) ?? 0) + 1);
|
|
||||||
|
|
||||||
if (this.shouldIncludeTopUsuario(usuarioKey, status)) {
|
|
||||||
userMap.set(usuario, (userMap.get(usuario) ?? 0) + 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
const faixa = this.resolveFranquiaLineBand(franquiaLine);
|
|
||||||
franquiaBandMap.set(faixa, (franquiaBandMap.get(faixa) ?? 0) + 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
const chipType = this.normalizeChipType(this.readLineString(line, 'tipoDeChip', 'TipoDeChip'));
|
|
||||||
if (chipType === 'ESIM') {
|
|
||||||
eSim += 1;
|
|
||||||
} else if (chipType === 'SIMCARD') {
|
|
||||||
simCard += 1;
|
|
||||||
} else {
|
|
||||||
outrosChip += 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const topPlanos = Array.from(planMap.entries())
|
|
||||||
.map(([plano, linhas]) => ({ plano, linhas }))
|
|
||||||
.sort((a, b) => b.linhas - a.linhas || a.plano.localeCompare(b.plano, 'pt-BR'))
|
|
||||||
.slice(0, this.resumoTopN);
|
|
||||||
|
|
||||||
const topUsuarios = Array.from(userMap.entries())
|
|
||||||
.map(([cliente, linhas]) => ({ cliente, linhas }))
|
|
||||||
.sort((a, b) => b.linhas - a.linhas || a.cliente.localeCompare(b.cliente, 'pt-BR'))
|
|
||||||
.slice(0, this.resumoTopN);
|
|
||||||
|
|
||||||
const franquiaOrder = ['Sem franquia', 'Até 10 GB', '10 a 20 GB', '20 a 50 GB', 'Acima de 50 GB'];
|
|
||||||
const franquiaLabels = franquiaOrder.filter((label) => (franquiaBandMap.get(label) ?? 0) > 0);
|
|
||||||
this.franquiaLabels = franquiaLabels.length ? franquiaLabels : franquiaOrder;
|
|
||||||
this.franquiaValues = this.franquiaLabels.map((label) => franquiaBandMap.get(label) ?? 0);
|
|
||||||
|
|
||||||
this.tipoChipLabels = ['e-SIM', 'SIMCARD', 'Outros'];
|
|
||||||
this.tipoChipValues = [eSim, simCard, outrosChip];
|
|
||||||
this.travelLabels = [];
|
|
||||||
this.travelValues = [];
|
|
||||||
this.adicionaisLabels = [];
|
|
||||||
this.adicionaisValues = [];
|
|
||||||
this.adicionaisTotals = [];
|
|
||||||
this.insights = null;
|
|
||||||
this.rebuildAdicionaisComparativo(null);
|
|
||||||
|
|
||||||
this.statusResumo = {
|
|
||||||
total: totalLinhas,
|
|
||||||
ativos: ativas,
|
|
||||||
bloqueadas,
|
|
||||||
perdaRoubo: 0,
|
|
||||||
bloq120: 0,
|
|
||||||
reservas,
|
|
||||||
outras: outrosStatus,
|
|
||||||
};
|
|
||||||
|
|
||||||
this.clientOverview = {
|
|
||||||
hasData: totalLinhas > 0,
|
|
||||||
totalLinhas,
|
|
||||||
ativas,
|
|
||||||
bloqueadas,
|
|
||||||
reservas,
|
|
||||||
franquiaLineTotalGb,
|
|
||||||
planosContratados: planMap.size,
|
|
||||||
usuariosComLinha: userMap.size,
|
|
||||||
outrosStatus,
|
|
||||||
};
|
|
||||||
|
|
||||||
this.resumoTopPlanos = topPlanos;
|
|
||||||
this.resumoPlanosLabels = topPlanos.map((x) => x.plano);
|
|
||||||
this.resumoPlanosValues = topPlanos.map((x) => x.linhas);
|
|
||||||
|
|
||||||
this.resumoTopClientes = topUsuarios;
|
|
||||||
this.resumoClientesLabels = topUsuarios.map((x) => x.cliente);
|
|
||||||
this.resumoClientesValues = topUsuarios.map((x) => x.linhas);
|
|
||||||
|
|
||||||
this.resumoTopReserva = [];
|
|
||||||
this.resumoReservaLabels = [];
|
|
||||||
this.resumoReservaValues = [];
|
|
||||||
|
|
||||||
this.resumoPfPjLabels = [];
|
|
||||||
this.resumoPfPjValues = [];
|
|
||||||
this.resumoDiferencaPjPf = {
|
|
||||||
pfLinhas: null,
|
|
||||||
pjLinhas: null,
|
|
||||||
totalLinhas: null,
|
|
||||||
};
|
|
||||||
|
|
||||||
this.resumo = null;
|
|
||||||
this.rebuildPrimaryKpis();
|
|
||||||
}
|
|
||||||
|
|
||||||
private clearClientDashboardState() {
|
|
||||||
this.clientOverview = {
|
|
||||||
hasData: false,
|
|
||||||
totalLinhas: 0,
|
|
||||||
ativas: 0,
|
|
||||||
bloqueadas: 0,
|
|
||||||
reservas: 0,
|
|
||||||
franquiaLineTotalGb: 0,
|
|
||||||
planosContratados: 0,
|
|
||||||
usuariosComLinha: 0,
|
|
||||||
outrosStatus: 0,
|
|
||||||
};
|
|
||||||
this.statusResumo = {
|
|
||||||
total: 0,
|
|
||||||
ativos: 0,
|
|
||||||
bloqueadas: 0,
|
|
||||||
perdaRoubo: 0,
|
|
||||||
bloq120: 0,
|
|
||||||
reservas: 0,
|
|
||||||
outras: 0,
|
|
||||||
};
|
|
||||||
this.franquiaLabels = [];
|
|
||||||
this.franquiaValues = [];
|
|
||||||
this.tipoChipLabels = [];
|
|
||||||
this.tipoChipValues = [];
|
|
||||||
this.resumoTopClientes = [];
|
|
||||||
this.resumoTopPlanos = [];
|
|
||||||
this.resumoTopReserva = [];
|
|
||||||
this.resumoPlanosLabels = [];
|
|
||||||
this.resumoPlanosValues = [];
|
|
||||||
this.resumoClientesLabels = [];
|
|
||||||
this.resumoClientesValues = [];
|
|
||||||
this.resumoReservaLabels = [];
|
|
||||||
this.resumoReservaValues = [];
|
|
||||||
this.rebuildPrimaryKpis();
|
|
||||||
this.destroyCharts();
|
|
||||||
this.destroyResumoCharts();
|
|
||||||
}
|
|
||||||
|
|
||||||
private isNetworkError(error: unknown): boolean {
|
private isNetworkError(error: unknown): boolean {
|
||||||
if (error instanceof HttpErrorResponse) {
|
if (error instanceof HttpErrorResponse) {
|
||||||
return error.status === 0;
|
return error.status === 0;
|
||||||
|
|
@ -714,10 +448,6 @@ export class Dashboard implements OnInit, AfterViewInit, OnDestroy {
|
||||||
}
|
}
|
||||||
|
|
||||||
onResumoTopNChange() {
|
onResumoTopNChange() {
|
||||||
if (this.isCliente) {
|
|
||||||
void this.loadClientDashboardData();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
this.buildResumoDerived();
|
this.buildResumoDerived();
|
||||||
this.tryBuildResumoCharts();
|
this.tryBuildResumoCharts();
|
||||||
}
|
}
|
||||||
|
|
@ -806,7 +536,6 @@ export class Dashboard implements OnInit, AfterViewInit, OnDestroy {
|
||||||
vivo: {
|
vivo: {
|
||||||
qtdLinhas: this.toNumberOrNull(this.readNode(vivoRaw, 'qtdLinhas', 'QtdLinhas')),
|
qtdLinhas: this.toNumberOrNull(this.readNode(vivoRaw, 'qtdLinhas', 'QtdLinhas')),
|
||||||
totalFranquiaGb: this.toNumberOrNull(this.readNode(vivoRaw, 'totalFranquiaGb', 'TotalFranquiaGb')),
|
totalFranquiaGb: this.toNumberOrNull(this.readNode(vivoRaw, 'totalFranquiaGb', 'TotalFranquiaGb')),
|
||||||
totalFranquiaLine: this.toNumberOrNull(this.readNode(vivoRaw, 'totalFranquiaLine', 'TotalFranquiaLine')),
|
|
||||||
totalBaseMensal: this.toNumberOrNull(this.readNode(vivoRaw, 'totalBaseMensal', 'TotalBaseMensal')),
|
totalBaseMensal: this.toNumberOrNull(this.readNode(vivoRaw, 'totalBaseMensal', 'TotalBaseMensal')),
|
||||||
totalAdicionaisMensal: this.toNumberOrNull(this.readNode(vivoRaw, 'totalAdicionaisMensal', 'TotalAdicionaisMensal')),
|
totalAdicionaisMensal: this.toNumberOrNull(this.readNode(vivoRaw, 'totalAdicionaisMensal', 'TotalAdicionaisMensal')),
|
||||||
totalGeralMensal: this.toNumberOrNull(this.readNode(vivoRaw, 'totalGeralMensal', 'TotalGeralMensal')),
|
totalGeralMensal: this.toNumberOrNull(this.readNode(vivoRaw, 'totalGeralMensal', 'TotalGeralMensal')),
|
||||||
|
|
@ -1054,7 +783,6 @@ export class Dashboard implements OnInit, AfterViewInit, OnDestroy {
|
||||||
}
|
}
|
||||||
|
|
||||||
private async loadFallbackFromLinesIfNeeded(force = false): Promise<void> {
|
private async loadFallbackFromLinesIfNeeded(force = false): Promise<void> {
|
||||||
if (this.isCliente) return;
|
|
||||||
if (!isPlatformBrowser(this.platformId) || this.fallbackInsightsLoading) return;
|
if (!isPlatformBrowser(this.platformId) || this.fallbackInsightsLoading) return;
|
||||||
|
|
||||||
const syncIndex = this.adicionaisLabels.findIndex(
|
const syncIndex = this.adicionaisLabels.findIndex(
|
||||||
|
|
@ -1173,58 +901,6 @@ export class Dashboard implements OnInit, AfterViewInit, OnDestroy {
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
|
|
||||||
private isReservaLine(line: DashboardLineListItemDto): boolean {
|
|
||||||
const cliente = this.normalizeSeriesKey(this.readLineString(line, 'cliente', 'Cliente'));
|
|
||||||
const usuario = this.normalizeSeriesKey(this.readLineString(line, 'usuario', 'Usuario'));
|
|
||||||
const skil = this.normalizeSeriesKey(this.readLineString(line, 'skil', 'Skil'));
|
|
||||||
return cliente === 'RESERVA' || usuario === 'RESERVA' || skil === 'RESERVA';
|
|
||||||
}
|
|
||||||
|
|
||||||
private shouldIncludeTopUsuario(usuarioKey: string, statusKey: string): boolean {
|
|
||||||
if (!usuarioKey) return false;
|
|
||||||
|
|
||||||
const invalidUserTokens = [
|
|
||||||
'SEMUSUARIO',
|
|
||||||
'AGUARDANDOUSUARIO',
|
|
||||||
'AGUARDANDO',
|
|
||||||
'BLOQUEAR',
|
|
||||||
'BLOQUEAD',
|
|
||||||
'BLOQUEADO',
|
|
||||||
'RESERVA',
|
|
||||||
'NAOATRIBUIDO',
|
|
||||||
'PENDENTE',
|
|
||||||
];
|
|
||||||
if (invalidUserTokens.some((token) => usuarioKey.includes(token))) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
const blockedStatusTokens = ['BLOQUE', 'PERDA', 'ROUBO', 'SUSPEN', 'CANCEL', 'AGUARD'];
|
|
||||||
return !blockedStatusTokens.some((token) => statusKey.includes(token));
|
|
||||||
}
|
|
||||||
|
|
||||||
private resolveFranquiaLineBand(value: number): string {
|
|
||||||
if (!Number.isFinite(value) || value <= 0) return 'Sem franquia';
|
|
||||||
if (value < 10) return 'Até 10 GB';
|
|
||||||
if (value < 20) return '10 a 20 GB';
|
|
||||||
if (value < 50) return '20 a 50 GB';
|
|
||||||
return 'Acima de 50 GB';
|
|
||||||
}
|
|
||||||
|
|
||||||
private extractDddFromLine(value: string | null | undefined): string | null {
|
|
||||||
const digits = (value ?? '').replace(/\D/g, '');
|
|
||||||
if (!digits) return null;
|
|
||||||
|
|
||||||
if (digits.startsWith('55') && digits.length >= 12) {
|
|
||||||
return digits.slice(2, 4);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (digits.length >= 10) {
|
|
||||||
return digits.slice(0, 2);
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
private destroyInsightsCharts() {
|
private destroyInsightsCharts() {
|
||||||
try { this.chartFranquia?.destroy(); } catch {}
|
try { this.chartFranquia?.destroy(); } catch {}
|
||||||
try { this.chartAdicionais?.destroy(); } catch {}
|
try { this.chartAdicionais?.destroy(); } catch {}
|
||||||
|
|
@ -1248,46 +924,36 @@ export class Dashboard implements OnInit, AfterViewInit, OnDestroy {
|
||||||
|
|
||||||
private rebuildPrimaryKpis() {
|
private rebuildPrimaryKpis() {
|
||||||
if (this.isCliente) {
|
if (this.isCliente) {
|
||||||
const overview = this.clientOverview;
|
this.kpis = [
|
||||||
const cards: KpiCard[] = [
|
|
||||||
{
|
{
|
||||||
key: 'linhas_total',
|
key: 'linhas_total',
|
||||||
title: 'Total de Linhas',
|
title: 'Total de Linhas',
|
||||||
value: this.formatInt(overview.totalLinhas),
|
value: this.formatInt(this.dashboardRaw?.totalLinhas ?? this.statusResumo.total),
|
||||||
icon: 'bi bi-sim-fill',
|
icon: 'bi bi-sim-fill',
|
||||||
hint: 'Base do cliente',
|
hint: 'Base geral',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'linhas_ativas',
|
key: 'linhas_ativas',
|
||||||
title: 'Linhas Ativas',
|
title: 'Linhas Ativas',
|
||||||
value: this.formatInt(overview.ativas),
|
value: this.formatInt(this.dashboardRaw?.ativos ?? this.statusResumo.ativos),
|
||||||
icon: 'bi bi-check2-circle',
|
icon: 'bi bi-check2-circle',
|
||||||
hint: 'Status ativo',
|
hint: 'Status ativo',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'franquia_line_total',
|
key: 'linhas_bloqueadas',
|
||||||
title: 'Franquia Line Total',
|
title: 'Linhas Bloqueadas',
|
||||||
value: this.formatDataAllowance(overview.franquiaLineTotalGb),
|
value: this.formatInt(this.dashboardRaw?.bloqueados ?? this.statusResumo.bloqueadas),
|
||||||
icon: 'bi bi-wifi',
|
icon: 'bi bi-slash-circle',
|
||||||
hint: 'Franquia contratada',
|
hint: 'Todos os bloqueios',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'planos_contratados',
|
key: 'linhas_reserva',
|
||||||
title: 'Planos Contratados',
|
title: 'Linhas em Reserva',
|
||||||
value: this.formatInt(overview.planosContratados),
|
value: this.formatInt(this.dashboardRaw?.reservas ?? this.statusResumo.reservas),
|
||||||
icon: 'bi bi-diagram-3-fill',
|
icon: 'bi bi-inboxes-fill',
|
||||||
hint: 'Planos ativos na base',
|
hint: 'Base de reserva',
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'usuarios_com_linha',
|
|
||||||
title: 'Usuários com Linha',
|
|
||||||
value: this.formatInt(overview.usuariosComLinha),
|
|
||||||
icon: 'bi bi-people-fill',
|
|
||||||
hint: 'Usuários vinculados',
|
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
this.kpis = cards;
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1306,33 +972,15 @@ export class Dashboard implements OnInit, AfterViewInit, OnDestroy {
|
||||||
add('linhas_ativas', 'Linhas Ativas', this.formatInt(dashboard.ativos), 'bi bi-check2-circle', 'Status ativo');
|
add('linhas_ativas', 'Linhas Ativas', this.formatInt(dashboard.ativos), 'bi bi-check2-circle', 'Status ativo');
|
||||||
add('linhas_bloqueadas', 'Linhas Bloqueadas', this.formatInt(dashboard.bloqueados), 'bi bi-slash-circle', 'Todos os bloqueios');
|
add('linhas_bloqueadas', 'Linhas Bloqueadas', this.formatInt(dashboard.bloqueados), 'bi bi-slash-circle', 'Todos os bloqueios');
|
||||||
add('linhas_reserva', 'Linhas em Reserva', this.formatInt(dashboard.reservas), 'bi bi-inboxes-fill', 'Base de reserva');
|
add('linhas_reserva', 'Linhas em Reserva', this.formatInt(dashboard.reservas), 'bi bi-inboxes-fill', 'Base de reserva');
|
||||||
const franquiaVivoTotal = this.toNumberOrNull(insights?.vivo?.totalFranquiaGb)
|
if (insights) {
|
||||||
?? this.toNumberOrNull(this.resumo?.vivoLineTotals?.franquiaTotal)
|
add(
|
||||||
?? (this.resumo?.vivoLineResumos ?? []).reduce(
|
'franquia_vivo_total',
|
||||||
(acc, row) => acc + (this.toNumberOrNull(row?.franquiaTotal) ?? 0),
|
'Total Franquia Vivo',
|
||||||
0
|
this.formatGb(this.toNumberOrNull(insights.vivo?.totalFranquiaGb) ?? 0),
|
||||||
|
'bi bi-diagram-3-fill',
|
||||||
|
'Soma das franquias (Geral)'
|
||||||
);
|
);
|
||||||
add(
|
}
|
||||||
'franquia_vivo_total',
|
|
||||||
'Total Franquia Vivo',
|
|
||||||
this.formatDataAllowance(franquiaVivoTotal),
|
|
||||||
'bi bi-diagram-3-fill',
|
|
||||||
'Soma das franquias (Geral)'
|
|
||||||
);
|
|
||||||
|
|
||||||
const franquiaLineTotal = this.toNumberOrNull(insights?.vivo?.totalFranquiaLine)
|
|
||||||
?? this.toNumberOrNull(this.resumo?.vivoLineTotals?.franquiaLine)
|
|
||||||
?? (this.resumo?.vivoLineResumos ?? []).reduce(
|
|
||||||
(acc, row) => acc + (this.toNumberOrNull(row?.franquiaLine) ?? 0),
|
|
||||||
0
|
|
||||||
);
|
|
||||||
add(
|
|
||||||
'franquia_line_total',
|
|
||||||
'Total Franquia Line',
|
|
||||||
this.formatDataAllowance(franquiaLineTotal),
|
|
||||||
'bi bi-hdd-network-fill',
|
|
||||||
'Soma da franquia line'
|
|
||||||
);
|
|
||||||
add('vig_vencidos', 'Vigencia Vencida', this.formatInt(dashboard.vigenciaVencidos), 'bi bi-exclamation-triangle-fill', 'Prioridade alta');
|
add('vig_vencidos', 'Vigencia Vencida', this.formatInt(dashboard.vigenciaVencidos), 'bi bi-exclamation-triangle-fill', 'Prioridade alta');
|
||||||
add('vig_30', 'Vence em 30 dias', this.formatInt(dashboard.vigenciaAVencer30), 'bi bi-calendar2-week-fill', 'Prioridade');
|
add('vig_30', 'Vence em 30 dias', this.formatInt(dashboard.vigenciaAVencer30), 'bi bi-calendar2-week-fill', 'Prioridade');
|
||||||
add('mureg_30', 'MUREG 30 dias', this.formatInt(dashboard.muregsUltimos30Dias), 'bi bi-arrow-repeat', 'Movimentacao');
|
add('mureg_30', 'MUREG 30 dias', this.formatInt(dashboard.muregsUltimos30Dias), 'bi bi-arrow-repeat', 'Movimentacao');
|
||||||
|
|
@ -1366,18 +1014,22 @@ 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.chartStatusPie?.nativeElement,
|
this.isCliente
|
||||||
this.chartAdicionaisComparativo?.nativeElement,
|
? [this.chartStatusPie?.nativeElement]
|
||||||
this.chartVigenciaMesAno?.nativeElement,
|
: [
|
||||||
this.chartVigenciaSupervisao?.nativeElement,
|
this.chartStatusPie?.nativeElement,
|
||||||
this.chartMureg12?.nativeElement,
|
this.chartAdicionaisComparativo?.nativeElement,
|
||||||
this.chartTroca12?.nativeElement,
|
this.chartVigenciaMesAno?.nativeElement,
|
||||||
this.chartLinhasPorFranquia?.nativeElement,
|
this.chartVigenciaSupervisao?.nativeElement,
|
||||||
this.chartAdicionaisPagos?.nativeElement,
|
this.chartMureg12?.nativeElement,
|
||||||
this.chartTipoChip?.nativeElement,
|
this.chartTroca12?.nativeElement,
|
||||||
this.chartTravelMundo?.nativeElement,
|
this.chartLinhasPorFranquia?.nativeElement,
|
||||||
].filter(Boolean) as HTMLCanvasElement[];
|
this.chartAdicionaisPagos?.nativeElement,
|
||||||
|
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)) {
|
||||||
|
|
@ -1402,8 +1054,7 @@ export class Dashboard implements OnInit, AfterViewInit, OnDestroy {
|
||||||
this.chartResumoReservaDdd?.nativeElement,
|
this.chartResumoReservaDdd?.nativeElement,
|
||||||
].filter(Boolean) as HTMLCanvasElement[];
|
].filter(Boolean) as HTMLCanvasElement[];
|
||||||
|
|
||||||
if (!canvases.length) return;
|
if (!canvases.length || canvases.some((c) => c.clientWidth === 0 || c.clientHeight === 0)) {
|
||||||
if (canvases.some((c) => c.clientWidth === 0 || c.clientHeight === 0)) {
|
|
||||||
this.scheduleResumoChartRetry();
|
this.scheduleResumoChartRetry();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -1432,10 +1083,10 @@ 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
|
const chartLabels = this.isCliente
|
||||||
? ['Ativas', 'Demais linhas']
|
? ['Ativas', 'Bloqueadas', 'Reservas']
|
||||||
: ['Ativos', 'Perda/Roubo', 'Bloq 120d', 'Reservas', 'Outros'];
|
: ['Ativos', 'Perda/Roubo', 'Bloq 120d', 'Reservas', 'Outros'];
|
||||||
const chartData = this.isCliente
|
const chartData = this.isCliente
|
||||||
? [this.statusResumo.ativos, this.clientDemaisLinhas]
|
? [this.statusResumo.ativos, this.statusResumo.bloqueadas, this.statusResumo.reservas]
|
||||||
: [
|
: [
|
||||||
this.statusResumo.ativos,
|
this.statusResumo.ativos,
|
||||||
this.statusResumo.perdaRoubo,
|
this.statusResumo.perdaRoubo,
|
||||||
|
|
@ -1444,7 +1095,7 @@ export class Dashboard implements OnInit, AfterViewInit, OnDestroy {
|
||||||
this.statusResumo.outras,
|
this.statusResumo.outras,
|
||||||
];
|
];
|
||||||
const chartColors = this.isCliente
|
const chartColors = this.isCliente
|
||||||
? [palette.status.ativos, '#cbd5e1']
|
? [palette.status.ativos, palette.status.blocked, palette.status.reserve]
|
||||||
: [
|
: [
|
||||||
palette.status.ativos,
|
palette.status.ativos,
|
||||||
palette.status.blocked,
|
palette.status.blocked,
|
||||||
|
|
@ -1473,6 +1124,10 @@ 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',
|
||||||
|
|
@ -1566,7 +1221,7 @@ export class Dashboard implements OnInit, AfterViewInit, OnDestroy {
|
||||||
labels: this.tipoChipLabels,
|
labels: this.tipoChipLabels,
|
||||||
datasets: [{
|
datasets: [{
|
||||||
data: this.tipoChipValues,
|
data: this.tipoChipValues,
|
||||||
backgroundColor: [palette.blue, palette.brand, '#94a3b8'],
|
backgroundColor: [palette.blue, palette.brand],
|
||||||
borderWidth: 0,
|
borderWidth: 0,
|
||||||
hoverOffset: 4
|
hoverOffset: 4
|
||||||
}]
|
}]
|
||||||
|
|
@ -1834,18 +1489,6 @@ export class Dashboard implements OnInit, AfterViewInit, OnDestroy {
|
||||||
const value = n.toLocaleString('pt-BR', { minimumFractionDigits: 0, maximumFractionDigits: 2 });
|
const value = n.toLocaleString('pt-BR', { minimumFractionDigits: 0, maximumFractionDigits: 2 });
|
||||||
return `${value} GB`;
|
return `${value} GB`;
|
||||||
}
|
}
|
||||||
|
|
||||||
formatDataAllowance(v: any) {
|
|
||||||
const n = this.toNumberOrNull(v);
|
|
||||||
if (n === null) return '0 GB';
|
|
||||||
if (n >= 1024) {
|
|
||||||
const tb = n / 1024;
|
|
||||||
const valueTb = tb.toLocaleString('pt-BR', { minimumFractionDigits: 0, maximumFractionDigits: 2 });
|
|
||||||
return `${valueTb} TB`;
|
|
||||||
}
|
|
||||||
const valueGb = n.toLocaleString('pt-BR', { minimumFractionDigits: 0, maximumFractionDigits: 2 });
|
|
||||||
return `${valueGb} GB`;
|
|
||||||
}
|
|
||||||
|
|
||||||
private toNumberOrNull(v: any) {
|
private toNumberOrNull(v: any) {
|
||||||
if (v === null || v === undefined || v === '') return null;
|
if (v === null || v === undefined || v === '') return null;
|
||||||
|
|
@ -1870,10 +1513,6 @@ export class Dashboard implements OnInit, AfterViewInit, OnDestroy {
|
||||||
return Number.isNaN(n) ? null : n;
|
return Number.isNaN(n) ? null : n;
|
||||||
}
|
}
|
||||||
|
|
||||||
get clientDemaisLinhas(): number {
|
|
||||||
return Math.max(0, (this.statusResumo.total ?? 0) - (this.statusResumo.ativos ?? 0));
|
|
||||||
}
|
|
||||||
|
|
||||||
trackByKpiKey = (_: number, item: KpiCard) => item.key;
|
trackByKpiKey = (_: number, item: KpiCard) => item.key;
|
||||||
|
|
||||||
private getPalette() {
|
private getPalette() {
|
||||||
|
|
|
||||||
|
|
@ -214,7 +214,7 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- KPIs -->
|
<!-- KPIs -->
|
||||||
<div class="geral-kpis mt-4 animate-fade-in" [class.geral-kpis-client]="isClientRestricted" *ngIf="isGroupMode">
|
<div class="geral-kpis mt-4 animate-fade-in" *ngIf="isGroupMode">
|
||||||
<div class="kpi" *ngIf="!isClientRestricted">
|
<div class="kpi" *ngIf="!isClientRestricted">
|
||||||
<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">
|
||||||
|
|
@ -313,7 +313,7 @@
|
||||||
{{ reservaSelectedCount > 0 && reservaSelectedCount === groupLines.length ? 'Limpar seleção' : 'Selecionar todas' }}
|
{{ reservaSelectedCount > 0 && reservaSelectedCount === groupLines.length ? 'Limpar seleção' : 'Selecionar todas' }}
|
||||||
</button>
|
</button>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
<ng-container *ngIf="isReservaExpandedGroup && hasGroupLineSelectionTools">
|
<ng-container *ngIf="isReservaExpandedGroup">
|
||||||
<button
|
<button
|
||||||
class="btn btn-sm btn-brand"
|
class="btn btn-sm btn-brand"
|
||||||
type="button"
|
type="button"
|
||||||
|
|
@ -697,20 +697,16 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-field">
|
<div class="form-field">
|
||||||
<label>Linha <span class="text-danger">*</span></label>
|
<label>
|
||||||
<app-select
|
{{ isCreateBatchMode ? 'Linha (Preencher no Lote)' : 'Linha' }}
|
||||||
class="form-select"
|
<span class="text-danger" *ngIf="!isCreateBatchMode">*</span>
|
||||||
size="sm"
|
</label>
|
||||||
[options]="createReservaLineOptions"
|
<input
|
||||||
labelKey="label"
|
class="form-control form-control-sm"
|
||||||
valueKey="value"
|
[(ngModel)]="createModel.linha"
|
||||||
[searchable]="true"
|
[disabled]="isCreateBatchMode"
|
||||||
searchPlaceholder="Pesquisar linha da reserva..."
|
[placeholder]="isCreateBatchMode ? 'Use a tabela de lote abaixo' : '119...'"
|
||||||
[placeholder]="loadingCreateReservaLines ? 'Carregando linhas da Reserva...' : 'Selecione uma linha da Reserva'"
|
/>
|
||||||
[disabled]="loadingCreateReservaLines"
|
|
||||||
[(ngModel)]="createModel.reservaLineId"
|
|
||||||
(ngModelChange)="onCreateReservaLineChange()"
|
|
||||||
></app-select>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-field">
|
<div class="form-field">
|
||||||
|
|
@ -1680,7 +1676,6 @@
|
||||||
*ngIf="detailOpen"
|
*ngIf="detailOpen"
|
||||||
#detailModal
|
#detailModal
|
||||||
class="modal-card modal-xl-custom"
|
class="modal-card modal-xl-custom"
|
||||||
[class.modal-client-detail]="isClientRestricted"
|
|
||||||
(click)="$event.stopPropagation()"
|
(click)="$event.stopPropagation()"
|
||||||
>
|
>
|
||||||
<div class="modal-header">
|
<div class="modal-header">
|
||||||
|
|
|
||||||
|
|
@ -251,19 +251,6 @@
|
||||||
|
|
||||||
/* KPIs */
|
/* KPIs */
|
||||||
.geral-kpis { display: grid; grid-template-columns: repeat(4, 1fr); gap: 12px; margin-top: 20px; margin-bottom: 16px; width: 100%; @media (max-width: 992px) { grid-template-columns: repeat(2, 1fr); } @media (max-width: 576px) { grid-template-columns: 1fr; } }
|
.geral-kpis { display: grid; grid-template-columns: repeat(4, 1fr); gap: 12px; margin-top: 20px; margin-bottom: 16px; width: 100%; @media (max-width: 992px) { grid-template-columns: repeat(2, 1fr); } @media (max-width: 576px) { grid-template-columns: 1fr; } }
|
||||||
.geral-kpis.geral-kpis-client {
|
|
||||||
grid-template-columns: repeat(3, minmax(180px, 240px));
|
|
||||||
justify-content: center;
|
|
||||||
|
|
||||||
@media (max-width: 992px) {
|
|
||||||
grid-template-columns: repeat(2, minmax(170px, 1fr));
|
|
||||||
justify-content: stretch;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 576px) {
|
|
||||||
grid-template-columns: 1fr;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.kpi { background: rgba(255,255,255,0.7); border: 1px solid rgba(17,18,20,0.08); border-radius: 16px; padding: 12px 16px; display: flex; justify-content: space-between; align-items: center; backdrop-filter: blur(8px); transition: transform 0.2s, box-shadow 0.2s; box-shadow: 0 2px 5px rgba(0,0,0,0.02); &:hover { transform: translateY(-2px); box-shadow: 0 6px 15px rgba(227, 61, 207, 0.1); background: #fff; border-color: var(--brand); } .lbl { font-size: 0.72rem; font-weight: 900; letter-spacing: 0.05em; text-transform: uppercase; color: var(--muted); &.text-success { color: var(--success-text) !important; } &.text-danger { color: var(--danger-text) !important; } } .val { font-size: 1.25rem; font-weight: 950; color: var(--text); } }
|
.kpi { background: rgba(255,255,255,0.7); border: 1px solid rgba(17,18,20,0.08); border-radius: 16px; padding: 12px 16px; display: flex; justify-content: space-between; align-items: center; backdrop-filter: blur(8px); transition: transform 0.2s, box-shadow 0.2s; box-shadow: 0 2px 5px rgba(0,0,0,0.02); &:hover { transform: translateY(-2px); box-shadow: 0 6px 15px rgba(227, 61, 207, 0.1); background: #fff; border-color: var(--brand); } .lbl { font-size: 0.72rem; font-weight: 900; letter-spacing: 0.05em; text-transform: uppercase; color: var(--muted); &.text-success { color: var(--success-text) !important; } &.text-danger { color: var(--danger-text) !important; } } .val { font-size: 1.25rem; font-weight: 950; color: var(--text); } }
|
||||||
.kpi .val-loading { font-size: 0.86rem; font-weight: 900; color: var(--muted); display: inline-flex; align-items: center; }
|
.kpi .val-loading { font-size: 0.86rem; font-weight: 900; color: var(--muted); display: inline-flex; align-items: center; }
|
||||||
|
|
||||||
|
|
@ -517,14 +504,6 @@
|
||||||
.modal-body { padding: 24px; overflow-y: auto; &.bg-light-gray { background-color: #f8f9fa; } }
|
.modal-body { padding: 24px; overflow-y: auto; &.bg-light-gray { background-color: #f8f9fa; } }
|
||||||
.modal-body .box-body { overflow: visible; }
|
.modal-body .box-body { overflow: visible; }
|
||||||
.modal-xl-custom { width: min(1100px, 95vw); max-height: 85vh; }
|
.modal-xl-custom { width: min(1100px, 95vw); max-height: 85vh; }
|
||||||
.modal-card.modal-client-detail {
|
|
||||||
width: min(560px, 95vw);
|
|
||||||
}
|
|
||||||
.modal-card.modal-client-detail .details-dashboard {
|
|
||||||
grid-template-columns: 1fr;
|
|
||||||
max-width: 520px;
|
|
||||||
margin: 0 auto;
|
|
||||||
}
|
|
||||||
.modal-card.modal-create { width: min(1280px, 96vw); max-height: 92vh; }
|
.modal-card.modal-create { width: min(1280px, 96vw); max-height: 92vh; }
|
||||||
.modal-card.modal-create.batch-mode { width: min(1560px, 99vw); }
|
.modal-card.modal-create.batch-mode { width: min(1560px, 99vw); }
|
||||||
.modal-card.modal-move-reserva {
|
.modal-card.modal-move-reserva {
|
||||||
|
|
|
||||||
|
|
@ -135,14 +135,6 @@ interface AccountCompanyOption {
|
||||||
contas: string[];
|
contas: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ReservaLineOption {
|
|
||||||
value: string;
|
|
||||||
label: string;
|
|
||||||
linha: string;
|
|
||||||
chip?: string;
|
|
||||||
usuario?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface CreateBatchLineDraft extends Partial<CreateMobileLineRequest> {
|
interface CreateBatchLineDraft extends Partial<CreateMobileLineRequest> {
|
||||||
uid: number;
|
uid: number;
|
||||||
linha: string;
|
linha: string;
|
||||||
|
|
@ -406,9 +398,6 @@ export class Geral implements OnInit, AfterViewInit, OnDestroy {
|
||||||
|
|
||||||
accountCompanies: AccountCompanyOption[] = [...this.fallbackAccountCompanies];
|
accountCompanies: AccountCompanyOption[] = [...this.fallbackAccountCompanies];
|
||||||
loadingAccountCompanies = false;
|
loadingAccountCompanies = false;
|
||||||
createReservaLineOptions: ReservaLineOption[] = [];
|
|
||||||
loadingCreateReservaLines = false;
|
|
||||||
private createReservaLineLookup = new Map<string, ReservaLineOption>();
|
|
||||||
|
|
||||||
get contaEmpresaOptions(): string[] {
|
get contaEmpresaOptions(): string[] {
|
||||||
return this.accountCompanies.map((x) => x.empresa);
|
return this.accountCompanies.map((x) => x.empresa);
|
||||||
|
|
@ -448,7 +437,6 @@ export class Geral implements OnInit, AfterViewInit, OnDestroy {
|
||||||
docType: 'PF',
|
docType: 'PF',
|
||||||
docNumber: '',
|
docNumber: '',
|
||||||
contaEmpresa: '',
|
contaEmpresa: '',
|
||||||
reservaLineId: '',
|
|
||||||
linha: '',
|
linha: '',
|
||||||
chip: '',
|
chip: '',
|
||||||
tipoDeChip: '',
|
tipoDeChip: '',
|
||||||
|
|
@ -579,7 +567,7 @@ export class Geral implements OnInit, AfterViewInit, OnDestroy {
|
||||||
}
|
}
|
||||||
|
|
||||||
get hasGroupLineSelectionTools(): boolean {
|
get hasGroupLineSelectionTools(): boolean {
|
||||||
return !this.isClientRestricted && !!(this.expandedGroup ?? '').trim();
|
return !!(this.expandedGroup ?? '').trim();
|
||||||
}
|
}
|
||||||
|
|
||||||
get canMoveSelectedLinesToReserva(): boolean {
|
get canMoveSelectedLinesToReserva(): boolean {
|
||||||
|
|
@ -2014,7 +2002,6 @@ export class Geral implements OnInit, AfterViewInit, OnDestroy {
|
||||||
this.createMode = 'NEW_CLIENT';
|
this.createMode = 'NEW_CLIENT';
|
||||||
this.resetCreateModel();
|
this.resetCreateModel();
|
||||||
this.createOpen = true;
|
this.createOpen = true;
|
||||||
void this.loadCreateReservaLines();
|
|
||||||
this.cdr.detectChanges();
|
this.cdr.detectChanges();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -2035,7 +2022,6 @@ export class Geral implements OnInit, AfterViewInit, OnDestroy {
|
||||||
this.syncContaEmpresaSelection(this.createModel);
|
this.syncContaEmpresaSelection(this.createModel);
|
||||||
|
|
||||||
this.createOpen = true;
|
this.createOpen = true;
|
||||||
void this.loadCreateReservaLines();
|
|
||||||
this.cdr.detectChanges();
|
this.cdr.detectChanges();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -2045,7 +2031,6 @@ export class Geral implements OnInit, AfterViewInit, OnDestroy {
|
||||||
docType: 'PF',
|
docType: 'PF',
|
||||||
docNumber: '',
|
docNumber: '',
|
||||||
contaEmpresa: '',
|
contaEmpresa: '',
|
||||||
reservaLineId: '',
|
|
||||||
linha: '',
|
linha: '',
|
||||||
chip: '',
|
chip: '',
|
||||||
tipoDeChip: '',
|
tipoDeChip: '',
|
||||||
|
|
@ -2716,7 +2701,7 @@ export class Geral implements OnInit, AfterViewInit, OnDestroy {
|
||||||
private buildCreatePayload(model: any): CreateMobileLineRequest {
|
private buildCreatePayload(model: any): CreateMobileLineRequest {
|
||||||
this.calculateFinancials(model);
|
this.calculateFinancials(model);
|
||||||
|
|
||||||
const { contaEmpresa: _contaEmpresa, uid: _uid, reservaLineId: _reservaLineId, ...createModelPayload } = model;
|
const { contaEmpresa: _contaEmpresa, uid: _uid, ...createModelPayload } = model;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...createModelPayload,
|
...createModelPayload,
|
||||||
|
|
@ -2831,99 +2816,6 @@ export class Geral implements OnInit, AfterViewInit, OnDestroy {
|
||||||
this.createModel.docNumber = value;
|
this.createModel.docNumber = value;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async loadCreateReservaLines(): Promise<void> {
|
|
||||||
if (this.loadingCreateReservaLines) return;
|
|
||||||
this.loadingCreateReservaLines = true;
|
|
||||||
|
|
||||||
try {
|
|
||||||
const pageSize = 500;
|
|
||||||
let page = 1;
|
|
||||||
const collected: ReservaLineOption[] = [];
|
|
||||||
|
|
||||||
while (true) {
|
|
||||||
const params = new HttpParams()
|
|
||||||
.set('page', String(page))
|
|
||||||
.set('pageSize', String(pageSize))
|
|
||||||
.set('skil', 'RESERVA');
|
|
||||||
|
|
||||||
const response = await firstValueFrom(this.http.get<ApiPagedResult<ApiLineList>>(this.apiBase, { params }));
|
|
||||||
const items = Array.isArray(response?.items) ? response.items : [];
|
|
||||||
|
|
||||||
for (const row of items) {
|
|
||||||
const id = (row?.id ?? '').toString().trim();
|
|
||||||
const linha = (row?.linha ?? '').toString().trim();
|
|
||||||
if (!id || !linha) continue;
|
|
||||||
collected.push({
|
|
||||||
value: id,
|
|
||||||
label: `${row?.item ?? ''} • ${linha} • ${(row?.usuario ?? 'SEM USUÁRIO').toString()}`,
|
|
||||||
linha,
|
|
||||||
chip: (row?.chip ?? '').toString(),
|
|
||||||
usuario: (row?.usuario ?? '').toString()
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const total = Number(response?.total ?? 0);
|
|
||||||
if (!items.length || (total > 0 && collected.length >= total) || items.length < pageSize) break;
|
|
||||||
page += 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
const seen = new Set<string>();
|
|
||||||
const unique = collected.filter((opt) => {
|
|
||||||
if (seen.has(opt.value)) return false;
|
|
||||||
seen.add(opt.value);
|
|
||||||
return true;
|
|
||||||
});
|
|
||||||
|
|
||||||
unique.sort((a, b) => a.linha.localeCompare(b.linha, 'pt-BR', { numeric: true, sensitivity: 'base' }));
|
|
||||||
|
|
||||||
this.createReservaLineOptions = unique;
|
|
||||||
this.createReservaLineLookup = new Map(unique.map((opt) => [opt.value, opt]));
|
|
||||||
} catch {
|
|
||||||
this.createReservaLineOptions = [];
|
|
||||||
this.createReservaLineLookup.clear();
|
|
||||||
await this.showToast('Erro ao carregar linhas da Reserva.');
|
|
||||||
} finally {
|
|
||||||
this.loadingCreateReservaLines = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onCreateReservaLineChange() {
|
|
||||||
const lineId = (this.createModel?.reservaLineId ?? '').toString().trim();
|
|
||||||
if (!lineId) {
|
|
||||||
this.createModel.linha = '';
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const selected = this.createReservaLineLookup.get(lineId);
|
|
||||||
if (selected) {
|
|
||||||
this.createModel.linha = selected.linha ?? '';
|
|
||||||
if (!String(this.createModel.chip ?? '').trim() && selected.chip) {
|
|
||||||
this.createModel.chip = selected.chip;
|
|
||||||
}
|
|
||||||
if (!String(this.createModel.usuario ?? '').trim() && selected.usuario) {
|
|
||||||
this.createModel.usuario = selected.usuario;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
this.http.get<ApiLineDetail>(`${this.apiBase}/${lineId}`).subscribe({
|
|
||||||
next: (detail) => {
|
|
||||||
this.createModel.linha = (detail?.linha ?? this.createModel.linha ?? '').toString();
|
|
||||||
if (!String(this.createModel.chip ?? '').trim() && detail?.chip) {
|
|
||||||
this.createModel.chip = detail.chip;
|
|
||||||
}
|
|
||||||
if (!String(this.createModel.tipoDeChip ?? '').trim() && detail?.tipoDeChip) {
|
|
||||||
this.createModel.tipoDeChip = detail.tipoDeChip;
|
|
||||||
}
|
|
||||||
if (!String(this.createModel.usuario ?? '').trim() && detail?.usuario) {
|
|
||||||
this.createModel.usuario = detail.usuario;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
error: () => {
|
|
||||||
// Mantém dados já carregados da lista.
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async saveCreate() {
|
async saveCreate() {
|
||||||
if (this.isCreateBatchMode) {
|
if (this.isCreateBatchMode) {
|
||||||
await this.saveCreateBatch();
|
await this.saveCreateBatch();
|
||||||
|
|
|
||||||
|
|
@ -9,8 +9,8 @@
|
||||||
<div class="title-badge">
|
<div class="title-badge">
|
||||||
<i class="bi bi-shield-lock-fill"></i> SYSADMIN
|
<i class="bi bi-shield-lock-fill"></i> SYSADMIN
|
||||||
</div>
|
</div>
|
||||||
<h1>Criar Credenciais do Cliente</h1>
|
<h1>Fornecer Usuário para Cliente</h1>
|
||||||
<p>Selecione o cliente e gere o acesso para acompanhamento das linhas no sistema.</p>
|
<p>Selecione um tenant-cliente e crie credenciais de acesso sem misturar tenants.</p>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
|
|
@ -20,10 +20,16 @@
|
||||||
|
|
||||||
<div class="alert-box success" *ngIf="successMessage">
|
<div class="alert-box success" *ngIf="successMessage">
|
||||||
{{ successMessage }}
|
{{ successMessage }}
|
||||||
|
<div class="mt-1" *ngIf="createdUser">
|
||||||
|
<small>
|
||||||
|
UserId: <strong>{{ createdUser.userId }}</strong> | TenantId:
|
||||||
|
<strong>{{ createdUser.tenantId }}</strong>
|
||||||
|
</small>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="alert-box error" *ngIf="submitErrors.length">
|
<div class="alert-box error" *ngIf="submitErrors.length">
|
||||||
<strong>Falha ao criar credencial:</strong>
|
<strong>Falha ao criar usuário:</strong>
|
||||||
<ul>
|
<ul>
|
||||||
<li *ngFor="let err of submitErrors">{{ err }}</li>
|
<li *ngFor="let err of submitErrors">{{ err }}</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
@ -32,32 +38,35 @@
|
||||||
<form [formGroup]="provisionForm" (ngSubmit)="onSubmit()" class="provision-form" novalidate>
|
<form [formGroup]="provisionForm" (ngSubmit)="onSubmit()" class="provision-form" novalidate>
|
||||||
<div class="form-grid">
|
<div class="form-grid">
|
||||||
<div class="form-field span-2">
|
<div class="form-field span-2">
|
||||||
<label for="tenantId">Cliente</label>
|
<label for="tenantId">Cliente (Tenant)</label>
|
||||||
<div class="select-row">
|
<div class="select-row">
|
||||||
<app-select
|
<select
|
||||||
id="tenantId"
|
id="tenantId"
|
||||||
class="form-control"
|
|
||||||
[options]="tenantOptions"
|
|
||||||
labelKey="label"
|
|
||||||
valueKey="value"
|
|
||||||
formControlName="tenantId"
|
formControlName="tenantId"
|
||||||
[disabled]="tenantsLoading || tenantOptions.length === 0"
|
class="form-control"
|
||||||
[searchable]="true"
|
[disabled]="tenantsLoading || !tenants.length"
|
||||||
searchPlaceholder="Pesquisar cliente..."
|
>
|
||||||
[placeholder]="tenantsLoading ? 'Carregando clientes...' : 'Selecione um cliente...'"
|
<option value="">Selecione um cliente...</option>
|
||||||
></app-select>
|
<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">
|
<button type="button" class="btn btn-ghost" (click)="loadTenants()" [disabled]="tenantsLoading">
|
||||||
{{ tenantsLoading ? 'Atualizando...' : 'Atualizar lista' }}
|
{{ tenantsLoading ? 'Atualizando...' : 'Atualizar lista' }}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
<small class="field-help">Origem: {{ sourceType }} (apenas tenants ativos).</small>
|
||||||
<small class="field-error" *ngIf="hasFieldError('tenantId', 'required')">
|
<small class="field-error" *ngIf="hasFieldError('tenantId', 'required')">
|
||||||
Selecione um cliente.
|
Selecione um tenant-cliente.
|
||||||
</small>
|
</small>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-field">
|
<div class="form-field">
|
||||||
<label for="name">Nome</label>
|
<label for="name">Nome (opcional)</label>
|
||||||
<input id="name" type="text" class="form-control" formControlName="name" placeholder="Nome do responsável" />
|
<input id="name" type="text" class="form-control" formControlName="name" placeholder="Nome do usuário" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-field">
|
<div class="form-field">
|
||||||
|
|
@ -102,11 +111,31 @@
|
||||||
</small>
|
</small>
|
||||||
<small class="field-error" *ngIf="passwordMismatch">As senhas não conferem.</small>
|
<small class="field-error" *ngIf="passwordMismatch">As senhas não conferem.</small>
|
||||||
</div>
|
</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>
|
||||||
|
|
||||||
<div class="form-actions">
|
<div class="form-actions">
|
||||||
<button type="submit" class="btn btn-primary" [disabled]="submitting || provisionForm.invalid">
|
<button type="submit" class="btn btn-primary" [disabled]="submitting || provisionForm.invalid">
|
||||||
<span *ngIf="!submitting">Criar credencial de acesso</span>
|
<span *ngIf="!submitting">Criar usuário para cliente</span>
|
||||||
<span *ngIf="submitting">Criando...</span>
|
<span *ngIf="submitting">Criando...</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,6 @@ import {
|
||||||
ValidationErrors,
|
ValidationErrors,
|
||||||
Validators,
|
Validators,
|
||||||
} from '@angular/forms';
|
} from '@angular/forms';
|
||||||
import { CustomSelectComponent } from '../../components/custom-select/custom-select';
|
|
||||||
|
|
||||||
import {
|
import {
|
||||||
SysadminService,
|
SysadminService,
|
||||||
|
|
@ -17,19 +16,30 @@ import {
|
||||||
CreateSystemTenantUserResponse,
|
CreateSystemTenantUserResponse,
|
||||||
} from '../../services/sysadmin.service';
|
} from '../../services/sysadmin.service';
|
||||||
|
|
||||||
|
type RoleOption = {
|
||||||
|
value: string;
|
||||||
|
label: string;
|
||||||
|
description: string;
|
||||||
|
};
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-system-provision-user',
|
selector: 'app-system-provision-user',
|
||||||
standalone: true,
|
standalone: true,
|
||||||
imports: [CommonModule, ReactiveFormsModule, CustomSelectComponent],
|
imports: [CommonModule, ReactiveFormsModule],
|
||||||
templateUrl: './system-provision-user.html',
|
templateUrl: './system-provision-user.html',
|
||||||
styleUrls: ['./system-provision-user.scss'],
|
styleUrls: ['./system-provision-user.scss'],
|
||||||
})
|
})
|
||||||
export class SystemProvisionUserPage implements OnInit {
|
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';
|
readonly sourceType = 'MobileLines.Cliente';
|
||||||
|
|
||||||
provisionForm: FormGroup;
|
provisionForm: FormGroup;
|
||||||
tenants: SystemTenantDto[] = [];
|
tenants: SystemTenantDto[] = [];
|
||||||
tenantOptions: Array<{ label: string; value: string }> = [];
|
|
||||||
tenantsLoading = false;
|
tenantsLoading = false;
|
||||||
tenantsError = '';
|
tenantsError = '';
|
||||||
|
|
||||||
|
|
@ -64,7 +74,6 @@ export class SystemProvisionUserPage implements OnInit {
|
||||||
|
|
||||||
this.tenantsLoading = true;
|
this.tenantsLoading = true;
|
||||||
this.tenantsError = '';
|
this.tenantsError = '';
|
||||||
this.syncTenantControlAvailability();
|
|
||||||
|
|
||||||
this.sysadminService
|
this.sysadminService
|
||||||
.listTenants({ source: this.sourceType, active: true })
|
.listTenants({ source: this.sourceType, active: true })
|
||||||
|
|
@ -73,26 +82,34 @@ export class SystemProvisionUserPage implements OnInit {
|
||||||
this.tenants = (tenants || []).slice().sort((a, b) =>
|
this.tenants = (tenants || []).slice().sort((a, b) =>
|
||||||
(a.nomeOficial || '').localeCompare(b.nomeOficial || '', 'pt-BR', { sensitivity: 'base' })
|
(a.nomeOficial || '').localeCompare(b.nomeOficial || '', 'pt-BR', { sensitivity: 'base' })
|
||||||
);
|
);
|
||||||
this.tenantOptions = this.tenants.map((tenant) => ({
|
|
||||||
label: tenant.nomeOficial || tenant.tenantId,
|
|
||||||
value: tenant.tenantId,
|
|
||||||
}));
|
|
||||||
this.tenantsLoading = false;
|
this.tenantsLoading = false;
|
||||||
this.syncTenantControlAvailability();
|
|
||||||
},
|
},
|
||||||
error: (err: HttpErrorResponse) => {
|
error: (err: HttpErrorResponse) => {
|
||||||
this.tenantsLoading = false;
|
this.tenantsLoading = false;
|
||||||
this.tenants = [];
|
|
||||||
this.tenantOptions = [];
|
|
||||||
this.tenantsError = this.extractErrorMessage(
|
this.tenantsError = this.extractErrorMessage(
|
||||||
err,
|
err,
|
||||||
'Não foi possível carregar os clientes. Verifique se a conta possui role sysadmin.'
|
'Não foi possível carregar os clientes. Verifique se a conta possui role sysadmin.'
|
||||||
);
|
);
|
||||||
this.syncTenantControlAvailability();
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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 {
|
onSubmit(): void {
|
||||||
if (this.submitting) return;
|
if (this.submitting) return;
|
||||||
|
|
||||||
|
|
@ -100,8 +117,11 @@ export class SystemProvisionUserPage implements OnInit {
|
||||||
this.submitErrors = [];
|
this.submitErrors = [];
|
||||||
this.createdUser = null;
|
this.createdUser = null;
|
||||||
|
|
||||||
if (this.provisionForm.invalid) {
|
if (this.provisionForm.invalid || this.selectedRoles.length === 0) {
|
||||||
this.provisionForm.markAllAsTouched();
|
this.provisionForm.markAllAsTouched();
|
||||||
|
if (this.selectedRoles.length === 0) {
|
||||||
|
this.submitErrors = ['Selecione ao menos uma role para o usuário.'];
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -118,8 +138,7 @@ export class SystemProvisionUserPage implements OnInit {
|
||||||
name: nameRaw,
|
name: nameRaw,
|
||||||
email,
|
email,
|
||||||
password,
|
password,
|
||||||
roles: ['cliente'],
|
roles: this.selectedRoles,
|
||||||
clientCredentialsOnly: true,
|
|
||||||
})
|
})
|
||||||
.subscribe({
|
.subscribe({
|
||||||
next: (created) => {
|
next: (created) => {
|
||||||
|
|
@ -129,7 +148,7 @@ export class SystemProvisionUserPage implements OnInit {
|
||||||
this.createdUser = created;
|
this.createdUser = created;
|
||||||
const tenant = this.findTenantById(created.tenantId) ?? this.findTenantById(tenantId);
|
const tenant = this.findTenantById(created.tenantId) ?? this.findTenantById(tenantId);
|
||||||
const tenantName = tenant?.nomeOficial || 'cliente selecionado';
|
const tenantName = tenant?.nomeOficial || 'cliente selecionado';
|
||||||
this.successMessage = `Credencial de acesso criada com sucesso para ${tenantName}.`;
|
this.successMessage = `Usuário ${created.email} criado com sucesso para ${tenantName}.`;
|
||||||
|
|
||||||
this.provisionForm.patchValue({
|
this.provisionForm.patchValue({
|
||||||
name: '',
|
name: '',
|
||||||
|
|
@ -153,6 +172,10 @@ export class SystemProvisionUserPage implements OnInit {
|
||||||
return tenant.tenantId;
|
return tenant.tenantId;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
trackByRoleValue(_: number, role: RoleOption): string {
|
||||||
|
return role.value;
|
||||||
|
}
|
||||||
|
|
||||||
hasFieldError(field: string, error?: string): boolean {
|
hasFieldError(field: string, error?: string): boolean {
|
||||||
const control = this.provisionForm.get(field);
|
const control = this.provisionForm.get(field);
|
||||||
if (!control) return false;
|
if (!control) return false;
|
||||||
|
|
@ -165,6 +188,15 @@ export class SystemProvisionUserPage implements OnInit {
|
||||||
return !!(confirmTouched && this.provisionForm.errors?.['passwordMismatch']);
|
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 {
|
private findTenantById(tenantId: string): SystemTenantDto | undefined {
|
||||||
return this.tenants.find((tenant) => tenant.tenantId === tenantId);
|
return this.tenants.find((tenant) => tenant.tenantId === tenantId);
|
||||||
}
|
}
|
||||||
|
|
@ -176,21 +208,6 @@ export class SystemProvisionUserPage implements OnInit {
|
||||||
}
|
}
|
||||||
|
|
||||||
this.provisionForm.enable({ emitEvent: false });
|
this.provisionForm.enable({ emitEvent: false });
|
||||||
this.syncTenantControlAvailability();
|
|
||||||
}
|
|
||||||
|
|
||||||
private syncTenantControlAvailability(): void {
|
|
||||||
const tenantControl = this.provisionForm.get('tenantId');
|
|
||||||
if (!tenantControl) return;
|
|
||||||
if (this.submitting) return;
|
|
||||||
|
|
||||||
const shouldDisable = this.tenantsLoading || this.tenants.length === 0;
|
|
||||||
if (shouldDisable) {
|
|
||||||
tenantControl.disable({ emitEvent: false });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
tenantControl.enable({ emitEvent: false });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private extractErrors(err: HttpErrorResponse): string[] {
|
private extractErrors(err: HttpErrorResponse): string[] {
|
||||||
|
|
@ -220,7 +237,7 @@ export class SystemProvisionUserPage implements OnInit {
|
||||||
return ['Acesso negado. Este recurso é exclusivo para sysadmin.'];
|
return ['Acesso negado. Este recurso é exclusivo para sysadmin.'];
|
||||||
}
|
}
|
||||||
|
|
||||||
return ['Não foi possível criar a credencial para o cliente selecionado.'];
|
return ['Não foi possível criar o usuário para o cliente selecionado.'];
|
||||||
}
|
}
|
||||||
|
|
||||||
private extractErrorMessage(err: HttpErrorResponse, fallback: string): string {
|
private extractErrorMessage(err: HttpErrorResponse, fallback: string): string {
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,6 @@ export type CreateSystemTenantUserPayload = {
|
||||||
email: string;
|
email: string;
|
||||||
password: string;
|
password: string;
|
||||||
roles: string[];
|
roles: string[];
|
||||||
clientCredentialsOnly?: boolean;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export type CreateSystemTenantUserResponse = {
|
export type CreateSystemTenantUserResponse = {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue