feat: tela de dashboard com kpis clicaveis e filtro bloqueados
This commit is contained in:
parent
79d372d67b
commit
56e1fc2379
|
|
@ -30,7 +30,14 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="hero-grid fade-in-up" [style.animation-delay]="'100ms'" *ngIf="!isCliente || clientOverview.hasData">
|
<div class="hero-grid fade-in-up" [style.animation-delay]="'100ms'" *ngIf="!isCliente || clientOverview.hasData">
|
||||||
<div class="hero-card" *ngFor="let k of kpis; trackBy: trackByKpiKey">
|
<div
|
||||||
|
class="hero-card"
|
||||||
|
*ngFor="let k of kpis; trackBy: trackByKpiKey"
|
||||||
|
[class.hero-card-clickable]="isKpiClickable(k)"
|
||||||
|
[attr.role]="isKpiClickable(k) ? 'button' : null"
|
||||||
|
[attr.tabindex]="isKpiClickable(k) ? 0 : null"
|
||||||
|
(click)="onKpiClick(k)"
|
||||||
|
(keydown)="onKpiCardKeydown($event, k)">
|
||||||
<div class="hero-icon">
|
<div class="hero-icon">
|
||||||
<i [class]="k.icon"></i>
|
<i [class]="k.icon"></i>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -178,6 +178,7 @@
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 12px;
|
gap: 12px;
|
||||||
|
cursor: default;
|
||||||
transition: all 0.2s ease;
|
transition: all 0.2s ease;
|
||||||
box-shadow: var(--shadow-sm);
|
box-shadow: var(--shadow-sm);
|
||||||
|
|
||||||
|
|
@ -189,6 +190,15 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.hero-card.hero-card-clickable {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-card.hero-card-clickable:focus-visible {
|
||||||
|
outline: 2px solid rgba(227, 61, 207, 0.7);
|
||||||
|
outline-offset: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
.hero-icon {
|
.hero-icon {
|
||||||
width: 40px;
|
width: 40px;
|
||||||
height: 40px;
|
height: 40px;
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,7 @@ import {
|
||||||
import { CommonModule, isPlatformBrowser } from '@angular/common';
|
import { CommonModule, isPlatformBrowser } from '@angular/common';
|
||||||
import { HttpClient, HttpErrorResponse, HttpParams } from '@angular/common/http';
|
import { HttpClient, HttpErrorResponse, HttpParams } from '@angular/common/http';
|
||||||
import { PLATFORM_ID } from '@angular/core';
|
import { PLATFORM_ID } from '@angular/core';
|
||||||
import { RouterModule } from '@angular/router';
|
import { RouterModule, Router } from '@angular/router';
|
||||||
import { firstValueFrom } from 'rxjs';
|
import { firstValueFrom } from 'rxjs';
|
||||||
|
|
||||||
import { environment } from '../../../environments/environment';
|
import { environment } from '../../../environments/environment';
|
||||||
|
|
@ -31,6 +31,11 @@ type KpiCard = {
|
||||||
hint?: string;
|
hint?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type KpiNavigationTarget = {
|
||||||
|
route: string;
|
||||||
|
queryParams?: Record<string, string>;
|
||||||
|
};
|
||||||
|
|
||||||
type SerieMesDto = {
|
type SerieMesDto = {
|
||||||
mes: string;
|
mes: string;
|
||||||
total: number;
|
total: number;
|
||||||
|
|
@ -354,11 +359,29 @@ export class Dashboard implements OnInit, AfterViewInit, OnDestroy {
|
||||||
private chartResumoReserva?: Chart;
|
private chartResumoReserva?: Chart;
|
||||||
|
|
||||||
private readonly baseApi: string;
|
private readonly baseApi: string;
|
||||||
|
private readonly kpiNavigationMap: Record<string, KpiNavigationTarget> = {
|
||||||
|
linhas_total: { route: '/geral' },
|
||||||
|
linhas_ativas: { route: '/geral' },
|
||||||
|
linhas_bloqueadas: { route: '/geral', queryParams: { statusMode: 'blocked' } },
|
||||||
|
linhas_reserva: { route: '/geral', queryParams: { skil: 'RESERVA' } },
|
||||||
|
franquia_vivo_total: { route: '/geral' },
|
||||||
|
franquia_line_total: { route: '/geral' },
|
||||||
|
vig_vencidos: { route: '/vigencia' },
|
||||||
|
vig_30: { route: '/vigencia' },
|
||||||
|
mureg_30: { route: '/mureg' },
|
||||||
|
troca_30: { route: '/trocanumero' },
|
||||||
|
cadastros_total: { route: '/dadosusuarios' },
|
||||||
|
travel_com: { route: '/geral', queryParams: { additionalMode: 'with', additionalServices: 'travel' } },
|
||||||
|
adicional_pago: { route: '/geral', queryParams: { additionalMode: 'with' } },
|
||||||
|
planos_contratados: { route: '/resumo', queryParams: { tab: 'planos' } },
|
||||||
|
usuarios_com_linha: { route: '/dadosusuarios' },
|
||||||
|
};
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private http: HttpClient,
|
private http: HttpClient,
|
||||||
private resumoService: ResumoService,
|
private resumoService: ResumoService,
|
||||||
private authService: AuthService,
|
private authService: AuthService,
|
||||||
|
private router: Router,
|
||||||
@Inject(PLATFORM_ID) private platformId: object
|
@Inject(PLATFORM_ID) private platformId: object
|
||||||
) {
|
) {
|
||||||
const raw = (environment.apiUrl || '').replace(/\/+$/, '');
|
const raw = (environment.apiUrl || '').replace(/\/+$/, '');
|
||||||
|
|
@ -1872,6 +1895,24 @@ export class Dashboard implements OnInit, AfterViewInit, OnDestroy {
|
||||||
|
|
||||||
trackByKpiKey = (_: number, item: KpiCard) => item.key;
|
trackByKpiKey = (_: number, item: KpiCard) => item.key;
|
||||||
|
|
||||||
|
isKpiClickable(card: KpiCard): boolean {
|
||||||
|
return !!this.kpiNavigationMap[card.key];
|
||||||
|
}
|
||||||
|
|
||||||
|
onKpiClick(card: KpiCard): void {
|
||||||
|
const target = this.kpiNavigationMap[card.key];
|
||||||
|
if (!target) return;
|
||||||
|
void this.router.navigate([target.route], {
|
||||||
|
queryParams: target.queryParams
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
onKpiCardKeydown(event: KeyboardEvent, card: KpiCard): void {
|
||||||
|
if (event.key !== 'Enter' && event.key !== ' ') return;
|
||||||
|
event.preventDefault();
|
||||||
|
this.onKpiClick(card);
|
||||||
|
}
|
||||||
|
|
||||||
private getPalette() {
|
private getPalette() {
|
||||||
return {
|
return {
|
||||||
brand: '#E33DCF',
|
brand: '#E33DCF',
|
||||||
|
|
|
||||||
|
|
@ -71,6 +71,27 @@
|
||||||
<i class="bi bi-archive me-1"></i> Reservas
|
<i class="bi bi-archive me-1"></i> Reservas
|
||||||
</button>
|
</button>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
|
<button type="button" class="filter-tab" [class.active]="filterStatus === 'BLOCKED'" (click)="toggleBlockedFilter()" [disabled]="loading">
|
||||||
|
<i class="bi bi-slash-circle me-1"></i> Bloqueadas
|
||||||
|
</button>
|
||||||
|
<ng-container *ngIf="filterStatus === 'BLOCKED'">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="filter-tab"
|
||||||
|
[class.active]="blockedStatusMode === 'PERDA_ROUBO'"
|
||||||
|
(click)="setBlockedStatusMode('PERDA_ROUBO')"
|
||||||
|
[disabled]="loading">
|
||||||
|
Perda/Roubo
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="filter-tab"
|
||||||
|
[class.active]="blockedStatusMode === 'BLOQUEIO_120'"
|
||||||
|
(click)="setBlockedStatusMode('BLOQUEIO_120')"
|
||||||
|
[disabled]="loading">
|
||||||
|
120 dias
|
||||||
|
</button>
|
||||||
|
</ng-container>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- CLIENTE MULTI-SELECT -->
|
<!-- CLIENTE MULTI-SELECT -->
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,7 @@ import {
|
||||||
HttpParams,
|
HttpParams,
|
||||||
HttpErrorResponse
|
HttpErrorResponse
|
||||||
} from '@angular/common/http';
|
} from '@angular/common/http';
|
||||||
import { NavigationEnd, Router } from '@angular/router';
|
import { ActivatedRoute, NavigationEnd, Router } from '@angular/router';
|
||||||
import { CustomSelectComponent } from '../../components/custom-select/custom-select';
|
import { CustomSelectComponent } from '../../components/custom-select/custom-select';
|
||||||
import { PlanAutoFillService } from '../../services/plan-autofill.service';
|
import { PlanAutoFillService } from '../../services/plan-autofill.service';
|
||||||
import { AuthService } from '../../services/auth.service';
|
import { AuthService } from '../../services/auth.service';
|
||||||
|
|
@ -42,6 +42,7 @@ type CreateMode = 'NEW_CLIENT' | 'NEW_LINE_IN_GROUP';
|
||||||
type CreateEntryMode = 'SINGLE' | 'BATCH';
|
type CreateEntryMode = 'SINGLE' | 'BATCH';
|
||||||
type AdditionalMode = 'ALL' | 'WITH' | 'WITHOUT';
|
type AdditionalMode = 'ALL' | 'WITH' | 'WITHOUT';
|
||||||
type AdditionalServiceKey = 'gvd' | 'skeelo' | 'news' | 'travel' | 'sync' | 'dispositivo';
|
type AdditionalServiceKey = 'gvd' | 'skeelo' | 'news' | 'travel' | 'sync' | 'dispositivo';
|
||||||
|
type BlockedStatusMode = 'ALL' | 'PERDA_ROUBO' | 'BLOQUEIO_120';
|
||||||
|
|
||||||
interface LineRow {
|
interface LineRow {
|
||||||
id: string;
|
id: string;
|
||||||
|
|
@ -290,6 +291,7 @@ export class Geral implements OnInit, AfterViewInit, OnDestroy {
|
||||||
private planAutoFill: PlanAutoFillService,
|
private planAutoFill: PlanAutoFillService,
|
||||||
private authService: AuthService,
|
private authService: AuthService,
|
||||||
private router: Router,
|
private router: Router,
|
||||||
|
private route: ActivatedRoute,
|
||||||
private tenantSyncService: TenantSyncService,
|
private tenantSyncService: TenantSyncService,
|
||||||
private solicitacoesLinhasService: SolicitacoesLinhasService
|
private solicitacoesLinhasService: SolicitacoesLinhasService
|
||||||
) {}
|
) {}
|
||||||
|
|
@ -317,6 +319,8 @@ export class Geral implements OnInit, AfterViewInit, OnDestroy {
|
||||||
|
|
||||||
searchTerm = '';
|
searchTerm = '';
|
||||||
filterSkil: 'ALL' | 'PF' | 'PJ' | 'RESERVA' = 'ALL';
|
filterSkil: 'ALL' | 'PF' | 'PJ' | 'RESERVA' = 'ALL';
|
||||||
|
filterStatus: 'ALL' | 'BLOCKED' = 'ALL';
|
||||||
|
blockedStatusMode: BlockedStatusMode = 'ALL';
|
||||||
additionalMode: AdditionalMode = 'ALL';
|
additionalMode: AdditionalMode = 'ALL';
|
||||||
selectedAdditionalServices: AdditionalServiceKey[] = [];
|
selectedAdditionalServices: AdditionalServiceKey[] = [];
|
||||||
readonly additionalServiceOptions: Array<{ key: AdditionalServiceKey; label: string }> = [
|
readonly additionalServiceOptions: Array<{ key: AdditionalServiceKey; label: string }> = [
|
||||||
|
|
@ -647,6 +651,10 @@ export class Geral implements OnInit, AfterViewInit, OnDestroy {
|
||||||
return this.additionalMode !== 'ALL' || this.selectedAdditionalServices.length > 0;
|
return this.additionalMode !== 'ALL' || this.selectedAdditionalServices.length > 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get hasClientSideFiltersApplied(): boolean {
|
||||||
|
return this.hasAdditionalFiltersApplied || this.filterStatus === 'BLOCKED';
|
||||||
|
}
|
||||||
|
|
||||||
get additionalModeLabel(): string {
|
get additionalModeLabel(): string {
|
||||||
if (this.additionalMode === 'WITH') return 'Com adicionais';
|
if (this.additionalMode === 'WITH') return 'Com adicionais';
|
||||||
if (this.additionalMode === 'WITHOUT') return 'Sem adicionais';
|
if (this.additionalMode === 'WITHOUT') return 'Sem adicionais';
|
||||||
|
|
@ -735,6 +743,8 @@ export class Geral implements OnInit, AfterViewInit, OnDestroy {
|
||||||
|
|
||||||
if (this.isClientRestricted) {
|
if (this.isClientRestricted) {
|
||||||
this.filterSkil = 'ALL';
|
this.filterSkil = 'ALL';
|
||||||
|
this.filterStatus = 'ALL';
|
||||||
|
this.blockedStatusMode = 'ALL';
|
||||||
this.additionalMode = 'ALL';
|
this.additionalMode = 'ALL';
|
||||||
this.selectedAdditionalServices = [];
|
this.selectedAdditionalServices = [];
|
||||||
this.selectedClients = [];
|
this.selectedClients = [];
|
||||||
|
|
@ -746,6 +756,7 @@ export class Geral implements OnInit, AfterViewInit, OnDestroy {
|
||||||
this.initAnimations();
|
this.initAnimations();
|
||||||
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
|
this.applyRouteFilters(this.route.snapshot.queryParams);
|
||||||
this.refreshData();
|
this.refreshData();
|
||||||
if (!this.isClientRestricted) {
|
if (!this.isClientRestricted) {
|
||||||
this.loadClients();
|
this.loadClients();
|
||||||
|
|
@ -766,9 +777,13 @@ export class Geral implements OnInit, AfterViewInit, OnDestroy {
|
||||||
this.navigationSub = this.router.events
|
this.navigationSub = this.router.events
|
||||||
.pipe(filter((event): event is NavigationEnd => event instanceof NavigationEnd))
|
.pipe(filter((event): event is NavigationEnd => event instanceof NavigationEnd))
|
||||||
.subscribe((event) => {
|
.subscribe((event) => {
|
||||||
const url = (event.urlAfterRedirects || '').toLowerCase();
|
const urlAfterRedirects = event.urlAfterRedirects || '';
|
||||||
|
const url = urlAfterRedirects.toLowerCase();
|
||||||
if (!url.includes('/geral')) return;
|
if (!url.includes('/geral')) return;
|
||||||
|
|
||||||
|
const parsed = this.router.parseUrl(urlAfterRedirects);
|
||||||
|
this.applyRouteFilters(parsed.queryParams ?? {});
|
||||||
|
|
||||||
this.searchResolvedClient = null;
|
this.searchResolvedClient = null;
|
||||||
if (!this.isClientRestricted) {
|
if (!this.isClientRestricted) {
|
||||||
this.loadClients();
|
this.loadClients();
|
||||||
|
|
@ -785,6 +800,137 @@ export class Geral implements OnInit, AfterViewInit, OnDestroy {
|
||||||
}, 100);
|
}, 100);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private applyRouteFilters(query: Record<string, unknown>): void {
|
||||||
|
const skil = this.parseQuerySkilFilter(query['skil']);
|
||||||
|
if (skil && (!this.isClientRestricted || skil === 'ALL')) {
|
||||||
|
this.filterSkil = skil;
|
||||||
|
}
|
||||||
|
|
||||||
|
const status = this.parseQueryStatusFilter(query['statusMode'] ?? query['statusFilter']);
|
||||||
|
if (status) {
|
||||||
|
this.filterStatus = status;
|
||||||
|
}
|
||||||
|
if (this.filterStatus !== 'BLOCKED') {
|
||||||
|
this.blockedStatusMode = 'ALL';
|
||||||
|
}
|
||||||
|
|
||||||
|
const blockedMode = this.parseQueryBlockedStatusMode(query['blockedMode'] ?? query['blockedType'] ?? query['statusSubtype']);
|
||||||
|
if (blockedMode) {
|
||||||
|
this.blockedStatusMode = blockedMode;
|
||||||
|
this.filterStatus = 'BLOCKED';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.isClientRestricted) {
|
||||||
|
const additionalMode = this.parseQueryAdditionalMode(query['additionalMode']);
|
||||||
|
if (additionalMode) {
|
||||||
|
this.additionalMode = additionalMode;
|
||||||
|
}
|
||||||
|
|
||||||
|
const additionalServices = this.parseQueryAdditionalServices(query['additionalServices']);
|
||||||
|
if (additionalServices) {
|
||||||
|
this.selectedAdditionalServices = additionalServices;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.expandedGroup = null;
|
||||||
|
this.groupLines = [];
|
||||||
|
this.selectedClients = [];
|
||||||
|
this.clientSearchTerm = '';
|
||||||
|
this.page = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
private parseQuerySkilFilter(value: unknown): 'ALL' | 'PF' | 'PJ' | 'RESERVA' | null {
|
||||||
|
const token = this.normalizeFilterToken(value);
|
||||||
|
if (!token) return null;
|
||||||
|
if (token === 'ALL' || token === 'TODOS') return 'ALL';
|
||||||
|
if (token === 'PF' || token === 'PESSOAFISICA') return 'PF';
|
||||||
|
if (token === 'PJ' || token === 'PESSOAJURIDICA') return 'PJ';
|
||||||
|
if (token === 'RESERVA' || token === 'RESERVAS') return 'RESERVA';
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private parseQueryStatusFilter(value: unknown): 'ALL' | 'BLOCKED' | null {
|
||||||
|
const token = this.normalizeFilterToken(value);
|
||||||
|
if (!token) return null;
|
||||||
|
if (token === 'ALL' || token === 'TODOS') return 'ALL';
|
||||||
|
if (
|
||||||
|
token === 'BLOCKED' ||
|
||||||
|
token === 'BLOQUEADAS' ||
|
||||||
|
token === 'BLOQUEADOS' ||
|
||||||
|
token === 'BLOQUEADA' ||
|
||||||
|
token === 'BLOQUEADO' ||
|
||||||
|
token === 'BLOQUEIO'
|
||||||
|
) {
|
||||||
|
return 'BLOCKED';
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private parseQueryBlockedStatusMode(value: unknown): BlockedStatusMode | null {
|
||||||
|
const token = this.normalizeFilterToken(value);
|
||||||
|
if (!token) return null;
|
||||||
|
if (token === 'ALL' || token === 'TODOS') return 'ALL';
|
||||||
|
if (
|
||||||
|
token === 'PERDAROUBO' ||
|
||||||
|
token === 'PERDAEROUBO' ||
|
||||||
|
token === 'PERDA' ||
|
||||||
|
token === 'ROUBO'
|
||||||
|
) {
|
||||||
|
return 'PERDA_ROUBO';
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
token === '120' ||
|
||||||
|
token === '120DIAS' ||
|
||||||
|
token === 'BLOQUEIO120' ||
|
||||||
|
token === 'BLOQUEIO120DIAS'
|
||||||
|
) {
|
||||||
|
return 'BLOQUEIO_120';
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private parseQueryAdditionalMode(value: unknown): AdditionalMode | null {
|
||||||
|
const token = this.normalizeFilterToken(value);
|
||||||
|
if (!token) return null;
|
||||||
|
if (token === 'ALL' || token === 'TODOS') return 'ALL';
|
||||||
|
if (token === 'WITH' || token === 'COM') return 'WITH';
|
||||||
|
if (token === 'WITHOUT' || token === 'SEM') return 'WITHOUT';
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private parseQueryAdditionalServices(value: unknown): AdditionalServiceKey[] | null {
|
||||||
|
if (value === undefined || value === null) return null;
|
||||||
|
const asString = Array.isArray(value) ? value.join(',') : String(value ?? '');
|
||||||
|
const chunks = asString
|
||||||
|
.split(',')
|
||||||
|
.map((part) => this.mapAdditionalServiceToken(part))
|
||||||
|
.filter((part): part is AdditionalServiceKey => !!part);
|
||||||
|
|
||||||
|
const unique = Array.from(new Set(chunks));
|
||||||
|
return unique;
|
||||||
|
}
|
||||||
|
|
||||||
|
private mapAdditionalServiceToken(value: unknown): AdditionalServiceKey | null {
|
||||||
|
const token = this.normalizeFilterToken(value);
|
||||||
|
if (!token) return null;
|
||||||
|
if (token === 'GVD' || token === 'GESTAOVOZDADOS' || token === 'GESTAOVOZEDADOS') return 'gvd';
|
||||||
|
if (token === 'SKEELO') return 'skeelo';
|
||||||
|
if (token === 'NEWS' || token === 'VIVONEWS' || token === 'VIVONEWSPLUS') return 'news';
|
||||||
|
if (token === 'TRAVEL' || token === 'TRAVELMUNDO' || token === 'VIVOTRAVELMUNDO') return 'travel';
|
||||||
|
if (token === 'SYNC' || token === 'VIVOSYNC') return 'sync';
|
||||||
|
if (token === 'DISPOSITIVO' || token === 'GESTAODISPOSITIVO' || token === 'VIVOGESTAODISPOSITIVO') return 'dispositivo';
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private normalizeFilterToken(value: unknown): string {
|
||||||
|
return String(value ?? '')
|
||||||
|
.normalize('NFD')
|
||||||
|
.replace(/[\u0300-\u036f]/g, '')
|
||||||
|
.replace(/[^A-Za-z0-9]/g, '')
|
||||||
|
.toUpperCase()
|
||||||
|
.trim();
|
||||||
|
}
|
||||||
|
|
||||||
private async loadPlanRules() {
|
private async loadPlanRules() {
|
||||||
try {
|
try {
|
||||||
await this.planAutoFill.load();
|
await this.planAutoFill.load();
|
||||||
|
|
@ -895,7 +1041,7 @@ export class Geral implements OnInit, AfterViewInit, OnDestroy {
|
||||||
refreshData(opts?: { keepCurrentPage?: boolean }) {
|
refreshData(opts?: { keepCurrentPage?: boolean }) {
|
||||||
const keepCurrentPage = !!opts?.keepCurrentPage;
|
const keepCurrentPage = !!opts?.keepCurrentPage;
|
||||||
this.keepPageOnNextGroupsLoad = keepCurrentPage;
|
this.keepPageOnNextGroupsLoad = keepCurrentPage;
|
||||||
if (!keepCurrentPage && this.filterSkil === 'RESERVA') {
|
if (!keepCurrentPage && (this.filterSkil === 'RESERVA' || this.filterStatus === 'BLOCKED')) {
|
||||||
this.page = 1;
|
this.page = 1;
|
||||||
}
|
}
|
||||||
this.searchResolvedClient = null;
|
this.searchResolvedClient = null;
|
||||||
|
|
@ -921,7 +1067,7 @@ export class Geral implements OnInit, AfterViewInit, OnDestroy {
|
||||||
const s = (term ?? '').trim();
|
const s = (term ?? '').trim();
|
||||||
if (!s) return Promise.resolve(null);
|
if (!s) return Promise.resolve(null);
|
||||||
|
|
||||||
const pageSize = this.hasAdditionalFiltersApplied ? '500' : '1';
|
const pageSize = this.hasClientSideFiltersApplied ? '500' : '1';
|
||||||
let params = new HttpParams().set('page', '1').set('pageSize', pageSize).set('search', s);
|
let params = new HttpParams().set('page', '1').set('pageSize', pageSize).set('search', s);
|
||||||
params = this.applyBaseFilters(params);
|
params = this.applyBaseFilters(params);
|
||||||
|
|
||||||
|
|
@ -932,7 +1078,7 @@ export class Geral implements OnInit, AfterViewInit, OnDestroy {
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
this.http.get<ApiPagedResult<ApiLineList>>(this.apiBase, { params: this.withNoCache(params) }).subscribe({
|
this.http.get<ApiPagedResult<ApiLineList>>(this.apiBase, { params: this.withNoCache(params) }).subscribe({
|
||||||
next: (res) => {
|
next: (res) => {
|
||||||
const source = this.hasAdditionalFiltersApplied
|
const source = this.hasClientSideFiltersApplied
|
||||||
? this.applyAdditionalFiltersClientSide(res.items ?? [])
|
? this.applyAdditionalFiltersClientSide(res.items ?? [])
|
||||||
: (res.items ?? []);
|
: (res.items ?? []);
|
||||||
const first = source[0];
|
const first = source[0];
|
||||||
|
|
@ -984,7 +1130,7 @@ export class Geral implements OnInit, AfterViewInit, OnDestroy {
|
||||||
const requestVersion = ++this.groupsRequestVersion;
|
const requestVersion = ++this.groupsRequestVersion;
|
||||||
this.loading = true;
|
this.loading = true;
|
||||||
|
|
||||||
if (this.hasAdditionalFiltersApplied) {
|
if (this.hasClientSideFiltersApplied) {
|
||||||
return this.loadOnlyThisClientGroupFromLines(clientName, requestVersion);
|
return this.loadOnlyThisClientGroupFromLines(clientName, requestVersion);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1051,7 +1197,7 @@ export class Geral implements OnInit, AfterViewInit, OnDestroy {
|
||||||
this.loadingClientsList = true;
|
this.loadingClientsList = true;
|
||||||
this.clientsList = [];
|
this.clientsList = [];
|
||||||
|
|
||||||
if (this.hasAdditionalFiltersApplied) {
|
if (this.hasClientSideFiltersApplied) {
|
||||||
void this.loadClientsFromLines(requestVersion);
|
void this.loadClientsFromLines(requestVersion);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -1147,6 +1293,47 @@ export class Geral implements OnInit, AfterViewInit, OnDestroy {
|
||||||
this.refreshData();
|
this.refreshData();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
toggleBlockedFilter() {
|
||||||
|
if (this.filterStatus === 'BLOCKED') {
|
||||||
|
this.filterStatus = 'ALL';
|
||||||
|
this.blockedStatusMode = 'ALL';
|
||||||
|
} else {
|
||||||
|
this.filterStatus = 'BLOCKED';
|
||||||
|
}
|
||||||
|
this.expandedGroup = null;
|
||||||
|
this.groupLines = [];
|
||||||
|
this.searchResolvedClient = null;
|
||||||
|
this.selectedClients = [];
|
||||||
|
this.clientSearchTerm = '';
|
||||||
|
this.page = 1;
|
||||||
|
|
||||||
|
if (!this.isClientRestricted) {
|
||||||
|
this.loadClients();
|
||||||
|
}
|
||||||
|
|
||||||
|
this.refreshData();
|
||||||
|
}
|
||||||
|
|
||||||
|
setBlockedStatusMode(mode: Exclude<BlockedStatusMode, 'ALL'>) {
|
||||||
|
if (this.filterStatus !== 'BLOCKED') {
|
||||||
|
this.filterStatus = 'BLOCKED';
|
||||||
|
}
|
||||||
|
|
||||||
|
this.blockedStatusMode = this.blockedStatusMode === mode ? 'ALL' : mode;
|
||||||
|
this.expandedGroup = null;
|
||||||
|
this.groupLines = [];
|
||||||
|
this.searchResolvedClient = null;
|
||||||
|
this.selectedClients = [];
|
||||||
|
this.clientSearchTerm = '';
|
||||||
|
this.page = 1;
|
||||||
|
|
||||||
|
if (!this.isClientRestricted) {
|
||||||
|
this.loadClients();
|
||||||
|
}
|
||||||
|
|
||||||
|
this.refreshData();
|
||||||
|
}
|
||||||
|
|
||||||
setAdditionalMode(mode: AdditionalMode) {
|
setAdditionalMode(mode: AdditionalMode) {
|
||||||
if (this.isClientRestricted) return;
|
if (this.isClientRestricted) return;
|
||||||
if (this.additionalMode === mode) return;
|
if (this.additionalMode === mode) return;
|
||||||
|
|
@ -1199,6 +1386,11 @@ export class Geral implements OnInit, AfterViewInit, OnDestroy {
|
||||||
if (this.filterSkil === 'PF') next = next.set('skil', 'PESSOA FÍSICA');
|
if (this.filterSkil === 'PF') next = next.set('skil', 'PESSOA FÍSICA');
|
||||||
else if (this.filterSkil === 'PJ') next = next.set('skil', 'PESSOA JURÍDICA');
|
else if (this.filterSkil === 'PJ') next = next.set('skil', 'PESSOA JURÍDICA');
|
||||||
else if (this.filterSkil === 'RESERVA') next = next.set('skil', 'RESERVA');
|
else if (this.filterSkil === 'RESERVA') next = next.set('skil', 'RESERVA');
|
||||||
|
if (this.filterStatus === 'BLOCKED') {
|
||||||
|
next = next.set('statusMode', 'blocked');
|
||||||
|
if (this.blockedStatusMode === 'PERDA_ROUBO') next = next.set('statusSubtype', 'perda_roubo');
|
||||||
|
else if (this.blockedStatusMode === 'BLOQUEIO_120') next = next.set('statusSubtype', '120_dias');
|
||||||
|
}
|
||||||
|
|
||||||
if (this.additionalMode === 'WITH') next = next.set('additionalMode', 'with');
|
if (this.additionalMode === 'WITH') next = next.set('additionalMode', 'with');
|
||||||
else if (this.additionalMode === 'WITHOUT') next = next.set('additionalMode', 'without');
|
else if (this.additionalMode === 'WITHOUT') next = next.set('additionalMode', 'without');
|
||||||
|
|
@ -1236,7 +1428,41 @@ export class Geral implements OnInit, AfterViewInit, OnDestroy {
|
||||||
(this.getAdditionalValue(line, 'dispositivo') > 0);
|
(this.getAdditionalValue(line, 'dispositivo') > 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private resolveBlockedStatusMode(status: unknown): Exclude<BlockedStatusMode, 'ALL'> | null {
|
||||||
|
const normalized = this.normalizeFilterToken(status);
|
||||||
|
if (!normalized) return null;
|
||||||
|
|
||||||
|
const hasBlockedToken =
|
||||||
|
normalized.includes('BLOQUE') ||
|
||||||
|
normalized.includes('PERDA') ||
|
||||||
|
normalized.includes('ROUBO') ||
|
||||||
|
normalized.includes('FURTO');
|
||||||
|
if (!hasBlockedToken) return null;
|
||||||
|
|
||||||
|
if (normalized.includes('120')) return 'BLOQUEIO_120';
|
||||||
|
if (normalized.includes('PERDA') || normalized.includes('ROUBO') || normalized.includes('FURTO')) {
|
||||||
|
return 'PERDA_ROUBO';
|
||||||
|
}
|
||||||
|
|
||||||
|
return 'PERDA_ROUBO';
|
||||||
|
}
|
||||||
|
|
||||||
|
private isBlockedStatus(status: unknown): boolean {
|
||||||
|
return this.resolveBlockedStatusMode(status) !== null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private matchesBlockedStatusMode(status: unknown): boolean {
|
||||||
|
const mode = this.resolveBlockedStatusMode(status);
|
||||||
|
if (!mode) return false;
|
||||||
|
if (this.blockedStatusMode === 'ALL') return true;
|
||||||
|
return mode === this.blockedStatusMode;
|
||||||
|
}
|
||||||
|
|
||||||
private matchesAdditionalFilters(line: ApiLineList): boolean {
|
private matchesAdditionalFilters(line: ApiLineList): boolean {
|
||||||
|
if (this.filterStatus === 'BLOCKED' && !this.matchesBlockedStatusMode(line?.status ?? '')) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
const selected = this.selectedAdditionalServices;
|
const selected = this.selectedAdditionalServices;
|
||||||
const hasSelected = selected.length > 0;
|
const hasSelected = selected.length > 0;
|
||||||
|
|
||||||
|
|
@ -1296,7 +1522,7 @@ export class Geral implements OnInit, AfterViewInit, OnDestroy {
|
||||||
}
|
}
|
||||||
|
|
||||||
private async fetchAllGroupsForKpis(): Promise<ClientGroupDto[]> {
|
private async fetchAllGroupsForKpis(): Promise<ClientGroupDto[]> {
|
||||||
if (this.hasAdditionalFiltersApplied) {
|
if (this.hasClientSideFiltersApplied) {
|
||||||
const lines = await this.fetchLinesForGrouping();
|
const lines = await this.fetchLinesForGrouping();
|
||||||
let groups = this.buildGroupsFromLines(lines);
|
let groups = this.buildGroupsFromLines(lines);
|
||||||
|
|
||||||
|
|
@ -1411,11 +1637,11 @@ export class Geral implements OnInit, AfterViewInit, OnDestroy {
|
||||||
const keepCurrentPage = this.keepPageOnNextGroupsLoad;
|
const keepCurrentPage = this.keepPageOnNextGroupsLoad;
|
||||||
this.keepPageOnNextGroupsLoad = false;
|
this.keepPageOnNextGroupsLoad = false;
|
||||||
|
|
||||||
if (!keepCurrentPage && this.filterSkil === 'RESERVA' && !hasSelection && !hasResolved) {
|
if (!keepCurrentPage && (this.filterSkil === 'RESERVA' || this.filterStatus === 'BLOCKED') && !hasSelection && !hasResolved) {
|
||||||
this.page = 1;
|
this.page = 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.hasAdditionalFiltersApplied) {
|
if (this.hasClientSideFiltersApplied) {
|
||||||
void this.loadGroupsFromLines(hasSelection, hasResolved, requestVersion);
|
void this.loadGroupsFromLines(hasSelection, hasResolved, requestVersion);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -1573,7 +1799,7 @@ export class Geral implements OnInit, AfterViewInit, OnDestroy {
|
||||||
|
|
||||||
const status = ((row?.status ?? '').toString().trim()).toLowerCase();
|
const status = ((row?.status ?? '').toString().trim()).toLowerCase();
|
||||||
if (status.includes('ativo')) group.ativos += 1;
|
if (status.includes('ativo')) group.ativos += 1;
|
||||||
if (status.includes('bloque') || status.includes('perda') || status.includes('roubo')) {
|
if (this.isBlockedStatus(row?.status ?? '')) {
|
||||||
group.bloqueados += 1;
|
group.bloqueados += 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue