Ajusta filtros e layout da pagina geral

This commit is contained in:
Eduardo Lopes 2026-03-12 16:08:17 -03:00
parent 9a635ac167
commit d1ec70cd69
6 changed files with 214 additions and 79 deletions

View File

@ -7,7 +7,7 @@
[attr.aria-disabled]="disabled" [attr.aria-disabled]="disabled"
> >
<span class="app-select-label">{{ displayLabel }}</span> <span class="app-select-label">{{ displayLabel }}</span>
<i class="bi bi-chevron-down"></i> <i class="bi bi-chevron-down app-select-chevron"></i>
</button> </button>
<div class="app-select-panel" *ngIf="isOpen"> <div class="app-select-panel" *ngIf="isOpen">

View File

@ -75,8 +75,7 @@
white-space: nowrap; white-space: nowrap;
} }
.app-select-trigger .app-select-chevron {
.app-select-trigger i {
position: absolute; position: absolute;
right: 8px; right: 8px;
top: 50%; top: 50%;

View File

@ -98,27 +98,19 @@
<button type="button" class="filter-tab" [class.active]="filterStatus === 'ACTIVE'" (click)="toggleActiveFilter()" [disabled]="loading"> <button type="button" class="filter-tab" [class.active]="filterStatus === 'ACTIVE'" (click)="toggleActiveFilter()" [disabled]="loading">
<i class="bi bi-check2-circle me-1"></i> Ativos <i class="bi bi-check2-circle me-1"></i> Ativos
</button> </button>
<button type="button" class="filter-tab" [class.active]="filterStatus === 'BLOCKED'" (click)="toggleBlockedFilter()" [disabled]="loading"> <div class="filter-blocked-select-box">
<i class="bi bi-slash-circle me-1"></i> Bloqueadas <i class="bi bi-slash-circle blocked-status-select-icon" aria-hidden="true"></i>
</button> <select
<ng-container *ngIf="filterStatus === 'BLOCKED'"> class="blocked-status-native-select"
<button [value]="blockedStatusMode"
type="button" (change)="onBlockedStatusSelectChange($event)"
class="filter-tab" [disabled]="loading"
[class.active]="blockedStatusMode === 'PERDA_ROUBO'" >
(click)="setBlockedStatusMode('PERDA_ROUBO')" <option *ngFor="let option of blockedStatusFilterOptions" [value]="option.value">
[disabled]="loading"> {{ option.label }}
Perda/Roubo </option>
</button> </select>
<button </div>
type="button"
class="filter-tab"
[class.active]="blockedStatusMode === 'BLOQUEIO_120'"
(click)="setBlockedStatusMode('BLOQUEIO_120')"
[disabled]="loading">
120 dias
</button>
</ng-container>
</div> </div>
</div> </div>
@ -347,34 +339,36 @@
</button> </button>
</div> </div>
<div class="page-size d-flex align-items-center gap-2"> <div class="controls-right">
<span class="text-muted small fw-bold text-uppercase" style="letter-spacing: 0.5px; font-size: 0.75rem;"> <div class="batch-status-tools" *ngIf="canManageLines">
Itens por pág: <span class="batch-status-count">
</span> Selecionadas: <strong>{{ batchStatusSelectionCount }}</strong>
</span>
<div class="select-wrapper"> <button
<app-select class="select-glass" size="sm" [options]="pageSizeOptions" [(ngModel)]="pageSize" (ngModelChange)="onPageSizeChange()" [disabled]="loading"></app-select> type="button"
class="btn btn-glass btn-sm"
(click)="openBatchStatusModal('BLOCK')"
[disabled]="!canOpenBatchStatusModal">
<i class="bi bi-lock me-1"></i> Bloquear em lote
</button>
<button
type="button"
class="btn btn-glass btn-sm"
(click)="openBatchStatusModal('UNBLOCK')"
[disabled]="!canOpenBatchStatusModal">
<i class="bi bi-unlock me-1"></i> Desbloquear em lote
</button>
</div> </div>
</div>
<div class="batch-status-tools" *ngIf="canManageLines"> <div class="page-size d-flex align-items-center gap-2">
<span class="batch-status-count"> <span class="text-muted small fw-bold text-uppercase" style="letter-spacing: 0.5px; font-size: 0.75rem;">
Selecionadas: <strong>{{ batchStatusSelectionCount }}</strong> Itens por pág:
</span> </span>
<button
type="button" <div class="select-wrapper">
class="btn btn-glass btn-sm" <app-select class="select-glass" size="sm" [options]="pageSizeOptions" [(ngModel)]="pageSize" (ngModelChange)="onPageSizeChange()" [disabled]="loading"></app-select>
(click)="openBatchStatusModal('BLOCK')" </div>
[disabled]="!canOpenBatchStatusModal"> </div>
<i class="bi bi-lock me-1"></i> Bloquear em lote
</button>
<button
type="button"
class="btn btn-glass btn-sm"
(click)="openBatchStatusModal('UNBLOCK')"
[disabled]="!canOpenBatchStatusModal">
<i class="bi bi-unlock me-1"></i> Desbloquear em lote
</button>
</div> </div>
</div> </div>
</div> </div>

View File

@ -143,13 +143,88 @@
.filters-row-top { .filters-row-top {
justify-content: center; justify-content: center;
z-index: 60;
} }
.filters-row-bottom { .filters-row-bottom {
justify-content: center; justify-content: center;
z-index: 40;
}
.filter-tabs { display: flex; gap: 4px; padding: 4px; background: rgba(255, 255, 255, 0.6); border: 1px solid rgba(17, 18, 20, 0.08); border-radius: 12px; backdrop-filter: blur(8px); flex-wrap: wrap; justify-content: center; }
.filter-tab { border: none; background: transparent; padding: 8px 16px; border-radius: 8px; font-size: 0.85rem; font-weight: 700; color: var(--muted); transition: all 0.2s ease; display: flex; align-items: center; gap: 6px; flex: 0 0 auto; white-space: nowrap; line-height: 1.1; &:hover { color: var(--text); background: rgba(255, 255, 255, 0.5); } &.active { background: #fff; color: var(--brand); box-shadow: 0 2px 8px rgba(227, 61, 207, 0.15); } &:disabled { opacity: 0.5; cursor: not-allowed; } }
.filter-blocked-select-box { width: 196px; flex: 0 0 auto; position: relative; z-index: 80; }
.blocked-status-select-icon {
position: absolute;
left: 11px;
top: 50%;
transform: translateY(-50%);
z-index: 2;
pointer-events: none;
color: rgba(17, 18, 20, 0.68);
font-size: 0.82rem;
}
.blocked-status-native-select {
width: 100%;
height: 36px;
border-radius: 12px;
border: 1px solid rgba(17, 18, 20, 0.15);
background: rgba(255, 255, 255, 0.7);
color: #0f172a;
font-size: 0.78rem;
font-weight: 800;
padding: 0 30px 0 30px;
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.04);
appearance: none;
-webkit-appearance: none;
transition: border-color 0.2s ease, box-shadow 0.2s ease, background-color 0.2s ease;
&:hover {
background: #fff;
border-color: rgba(17, 18, 20, 0.7);
box-shadow: 0 4px 12px rgba(3, 15, 170, 0.1);
}
&:focus {
outline: none;
border-color: #e33dcf;
box-shadow: 0 0 0 3px rgba(227, 61, 207, 0.15);
}
&:disabled {
background-color: #f1f5f9;
color: #94a3b8;
cursor: not-allowed;
}
}
@media (max-width: 1366px) {
.filter-tabs {
gap: 3px;
padding: 3px;
}
.filter-tab {
padding: 7px 12px;
font-size: 0.78rem;
gap: 5px;
}
.filter-blocked-select-box {
width: 184px;
}
}
@media (max-width: 1200px) {
.filter-tab {
padding: 6px 10px;
font-size: 0.74rem;
gap: 4px;
}
.filter-blocked-select-box {
width: 176px;
}
} }
.filter-tabs { display: flex; gap: 4px; padding: 4px; background: rgba(255, 255, 255, 0.6); border: 1px solid rgba(17, 18, 20, 0.08); border-radius: 12px; backdrop-filter: blur(8px); }
.filter-tab { border: none; background: transparent; padding: 8px 16px; border-radius: 8px; font-size: 0.85rem; font-weight: 700; color: var(--muted); transition: all 0.2s ease; display: flex; align-items: center; gap: 6px; &:hover { color: var(--text); background: rgba(255, 255, 255, 0.5); } &.active { background: #fff; color: var(--brand); box-shadow: 0 2px 8px rgba(227, 61, 207, 0.15); } &:disabled { opacity: 0.5; cursor: not-allowed; } }
.client-filter-wrap { position: relative; z-index: 40; } .client-filter-wrap { position: relative; z-index: 40; }
.btn-client-filter { display: flex; align-items: center; gap: 8px; padding: 6px 12px; border-radius: 12px; border: 1px solid rgba(17, 18, 20, 0.08); background: rgba(255, 255, 255, 0.6); color: var(--muted); font-weight: 700; font-size: 0.85rem; backdrop-filter: blur(8px); transition: all 0.2s; min-height: 38px; height: auto; flex-wrap: wrap; &:hover { background: #fff; border-color: var(--blue); color: var(--blue); } &.active, &.has-selection { background: #fff; border-color: var(--brand); } } .btn-client-filter { display: flex; align-items: center; gap: 8px; padding: 6px 12px; border-radius: 12px; border: 1px solid rgba(17, 18, 20, 0.08); background: rgba(255, 255, 255, 0.6); color: var(--muted); font-weight: 700; font-size: 0.85rem; backdrop-filter: blur(8px); transition: all 0.2s; min-height: 38px; height: auto; flex-wrap: wrap; &:hover { background: #fff; border-color: var(--blue); color: var(--blue); } &.active, &.has-selection { background: #fff; border-color: var(--brand); } }
@ -335,17 +410,32 @@
/* Controls */ /* Controls */
.controls { display: flex; gap: 12px; align-items: center; justify-content: space-between; flex-wrap: wrap; } .controls { display: flex; gap: 12px; align-items: center; justify-content: space-between; flex-wrap: wrap; }
.controls-right {
margin-left: auto;
display: flex;
align-items: center;
justify-content: flex-end;
gap: 12px;
flex-wrap: wrap;
@media (max-width: 900px) {
width: 100%;
justify-content: space-between;
}
@media (max-width: 500px) {
gap: 8px;
}
}
.search-group { max-width: 270px; border-radius: 12px; overflow: hidden; display: flex; align-items: stretch; background: #fff; border: 1px solid rgba(17, 18, 20, 0.15); box-shadow: 0 2px 6px rgba(0, 0, 0, 0.04); transition: all 0.2s ease; &:focus-within { border-color: var(--brand); box-shadow: 0 4px 12px rgba(227, 61, 207, 0.15); transform: translateY(-1px); } .input-group-text { background: transparent; border: none; color: var(--muted); padding-left: 14px; padding-right: 8px; display: flex; align-items: center; i { font-size: 1rem; } } .form-control { border: none; background: transparent; padding: 10px 0; font-size: 0.9rem; color: var(--text); box-shadow: none; &::placeholder { color: rgba(17, 18, 20, 0.4); font-weight: 500; } &:focus { outline: none; } } .btn-clear { background: transparent; border: none; color: var(--muted); padding: 0 12px; display: flex; align-items: center; cursor: pointer; transition: color 0.2s; &:hover { color: #dc3545; } i { font-size: 1rem; } } } .search-group { max-width: 270px; border-radius: 12px; overflow: hidden; display: flex; align-items: stretch; background: #fff; border: 1px solid rgba(17, 18, 20, 0.15); box-shadow: 0 2px 6px rgba(0, 0, 0, 0.04); transition: all 0.2s ease; &:focus-within { border-color: var(--brand); box-shadow: 0 4px 12px rgba(227, 61, 207, 0.15); transform: translateY(-1px); } .input-group-text { background: transparent; border: none; color: var(--muted); padding-left: 14px; padding-right: 8px; display: flex; align-items: center; i { font-size: 1rem; } } .form-control { border: none; background: transparent; padding: 10px 0; font-size: 0.9rem; color: var(--text); box-shadow: none; &::placeholder { color: rgba(17, 18, 20, 0.4); font-weight: 500; } &:focus { outline: none; } } .btn-clear { background: transparent; border: none; color: var(--muted); padding: 0 12px; display: flex; align-items: center; cursor: pointer; transition: color 0.2s; &:hover { color: #dc3545; } i { font-size: 1rem; } } }
.page-size { margin-left: auto; @media (max-width: 500px) { margin-left: 0; width: 100%; justify-content: space-between; } } .page-size { @media (max-width: 500px) { width: 100%; justify-content: space-between; } }
.batch-status-tools { .batch-status-tools {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 8px; gap: 8px;
flex-wrap: wrap; flex-wrap: wrap;
margin-left: auto;
@media (max-width: 900px) { @media (max-width: 900px) {
margin-left: 0;
width: 100%; width: 100%;
} }
} }

View File

@ -144,6 +144,23 @@ describe('Geral', () => {
expect(filtered[0].status).toBe('ATIVO'); expect(filtered[0].status).toBe('ATIVO');
}); });
it('should filter only pre-activation blocked lines when PRE_ATIVACAO is selected', () => {
component.filterStatus = 'BLOCKED';
component.blockedStatusMode = 'PRE_ATIVACAO';
component.filterOperadora = 'ALL';
component.filterContaEmpresa = '';
component.additionalMode = 'ALL';
component.selectedAdditionalServices = [];
const filtered = (component as any).applyAdditionalFiltersClientSide([
{ id: '1', item: 1, conta: '1', linha: '11911111111', cliente: 'A', usuario: 'U', vencConta: null, status: 'BLOQUEIO DE PRE ATIVACAO' },
{ id: '2', item: 2, conta: '2', linha: '11922222222', cliente: 'B', usuario: 'U', vencConta: null, status: 'BLOQUEIO 120 DIAS' },
]);
expect(filtered.length).toBe(1);
expect(filtered[0].status).toBe('BLOQUEIO DE PRE ATIVACAO');
});
it('should classify active line as ACTIVE during smart search resolution', () => { it('should classify active line as ACTIVE during smart search resolution', () => {
const target = (component as any).buildSmartSearchTarget({ const target = (component as any).buildSmartSearchTarget({
id: '1', id: '1',
@ -160,6 +177,33 @@ describe('Geral', () => {
expect(target?.statusFilter).toBe('ACTIVE'); expect(target?.statusFilter).toBe('ACTIVE');
}); });
it('should classify pre-activation line as blocked during smart search resolution', () => {
const target = (component as any).buildSmartSearchTarget({
id: '1',
item: 1,
conta: '1',
linha: '11911111111',
cliente: 'CLIENTE A',
usuario: 'USUARIO A',
vencConta: null,
status: 'BLOQUEIO DE PRE ATIVACAO',
skil: 'PESSOA JURIDICA',
}, true);
expect(target?.statusFilter).toBe('BLOCKED');
expect(target?.blockedStatusMode).toBe('PRE_ATIVACAO');
});
it('should toggle blocked select filter off when selecting the same blocked option again', () => {
component.setBlockedStatusFilter('BLOQUEIO_120');
expect(component.filterStatus).toBe('BLOCKED');
expect(component.blockedStatusMode).toBe('BLOQUEIO_120');
component.setBlockedStatusFilter('BLOQUEIO_120');
expect(component.filterStatus).toBe('ALL');
expect(component.blockedStatusMode).toBe('ALL');
});
it('should request assigned reserve lines in ALL filter only', () => { it('should request assigned reserve lines in ALL filter only', () => {
component.filterSkil = 'ALL'; component.filterSkil = 'ALL';
let params = (component as any).applyBaseFilters(new HttpParams()); let params = (component as any).applyBaseFilters(new HttpParams());

View File

@ -68,7 +68,7 @@ type CreateEntryMode = 'SINGLE' | 'BATCH';
type AdditionalMode = 'ALL' | 'WITH' | 'WITHOUT'; type AdditionalMode = 'ALL' | 'WITH' | 'WITHOUT';
type OperadoraFilterMode = 'ALL' | 'VIVO' | 'CLARO' | 'TIM'; type OperadoraFilterMode = 'ALL' | 'VIVO' | 'CLARO' | 'TIM';
type AdditionalServiceKey = 'gvd' | 'skeelo' | 'news' | 'travel' | 'sync' | 'dispositivo'; type AdditionalServiceKey = 'gvd' | 'skeelo' | 'news' | 'travel' | 'sync' | 'dispositivo';
type BlockedStatusMode = 'ALL' | 'PERDA_ROUBO' | 'BLOQUEIO_120'; type BlockedStatusMode = 'ALL' | 'PERDA_ROUBO' | 'BLOQUEIO_120' | 'PRE_ATIVACAO';
type SkilFilterMode = 'ALL' | 'PF' | 'PJ' | 'RESERVA' | 'ESTOQUE'; type SkilFilterMode = 'ALL' | 'PF' | 'PJ' | 'RESERVA' | 'ESTOQUE';
interface LineRow { interface LineRow {
@ -418,6 +418,12 @@ export class Geral implements OnInit, AfterViewInit, OnDestroy {
selectedAdditionalServices: AdditionalServiceKey[] = []; selectedAdditionalServices: AdditionalServiceKey[] = [];
filterOperadora: OperadoraFilterMode = 'ALL'; filterOperadora: OperadoraFilterMode = 'ALL';
filterContaEmpresa = ''; filterContaEmpresa = '';
readonly blockedStatusFilterOptions: Array<{ label: string; value: BlockedStatusMode }> = [
{ label: 'Bloqueadas', value: 'ALL' },
{ label: 'Bloqueio Perda/Roubo', value: 'PERDA_ROUBO' },
{ label: 'Bloqueio por 120 dias', value: 'BLOQUEIO_120' },
{ label: 'Bloqueio de Pré Ativação', value: 'PRE_ATIVACAO' },
];
readonly additionalServiceOptions: Array<{ key: AdditionalServiceKey; label: string }> = [ readonly additionalServiceOptions: Array<{ key: AdditionalServiceKey; label: string }> = [
{ key: 'gvd', label: 'Gestão Voz e Dados' }, { key: 'gvd', label: 'Gestão Voz e Dados' },
{ key: 'skeelo', label: 'Skeelo' }, { key: 'skeelo', label: 'Skeelo' },
@ -533,7 +539,7 @@ export class Geral implements OnInit, AfterViewInit, OnDestroy {
kpiAtivas = 0; kpiAtivas = 0;
kpiBloqueadas = 0; kpiBloqueadas = 0;
readonly statusOptions = ['ATIVO', 'BLOQUEIO PERDA/ROUBO', 'BLOQUEIO 120 DIAS']; readonly statusOptions = ['ATIVO', 'BLOQUEIO PERDA/ROUBO', 'BLOQUEIO 120 DIAS', 'BLOQUEIO DE PRÉ ATIVAÇÃO'];
readonly skilOptions = ['PESSOA FÍSICA', 'PESSOA JURÍDICA', 'RESERVA']; readonly skilOptions = ['PESSOA FÍSICA', 'PESSOA JURÍDICA', 'RESERVA'];
planOptions = [ planOptions = [
@ -1214,6 +1220,13 @@ export class Geral implements OnInit, AfterViewInit, OnDestroy {
) { ) {
return 'BLOQUEIO_120'; return 'BLOQUEIO_120';
} }
if (
token === 'PREATIVACAO' ||
token === 'PREATIV' ||
token === 'BLOQUEIOPREATIVACAO'
) {
return 'PRE_ATIVACAO';
}
return null; return null;
} }
@ -1845,13 +1858,18 @@ export class Geral implements OnInit, AfterViewInit, OnDestroy {
this.refreshData(); this.refreshData();
} }
toggleBlockedFilter() { setBlockedStatusFilter(mode: BlockedStatusMode) {
if (this.filterStatus === 'BLOCKED') { const normalizedMode = mode ?? 'ALL';
const sameSelection = this.filterStatus === 'BLOCKED' && this.blockedStatusMode === normalizedMode;
if (sameSelection) {
this.filterStatus = 'ALL'; this.filterStatus = 'ALL';
this.blockedStatusMode = 'ALL'; this.blockedStatusMode = 'ALL';
} else { } else {
this.filterStatus = 'BLOCKED'; this.filterStatus = 'BLOCKED';
this.blockedStatusMode = normalizedMode;
} }
this.expandedGroup = null; this.expandedGroup = null;
this.groupLines = []; this.groupLines = [];
this.searchResolvedClient = null; this.searchResolvedClient = null;
@ -1866,24 +1884,10 @@ export class Geral implements OnInit, AfterViewInit, OnDestroy {
this.refreshData(); this.refreshData();
} }
setBlockedStatusMode(mode: Exclude<BlockedStatusMode, 'ALL'>) { onBlockedStatusSelectChange(event: Event) {
if (this.filterStatus !== 'BLOCKED') { const target = event.target;
this.filterStatus = 'BLOCKED'; const value = target instanceof HTMLSelectElement ? target.value : 'ALL';
} this.setBlockedStatusFilter(value as BlockedStatusMode);
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) {
@ -1974,6 +1978,7 @@ export class Geral implements OnInit, AfterViewInit, OnDestroy {
next = next.set('statusMode', 'blocked'); next = next.set('statusMode', 'blocked');
if (this.blockedStatusMode === 'PERDA_ROUBO') next = next.set('statusSubtype', 'perda_roubo'); if (this.blockedStatusMode === 'PERDA_ROUBO') next = next.set('statusSubtype', 'perda_roubo');
else if (this.blockedStatusMode === 'BLOQUEIO_120') next = next.set('statusSubtype', '120_dias'); else if (this.blockedStatusMode === 'BLOQUEIO_120') next = next.set('statusSubtype', '120_dias');
else if (this.blockedStatusMode === 'PRE_ATIVACAO') next = next.set('statusSubtype', 'pre_ativacao');
} }
if (this.additionalMode === 'WITH') next = next.set('additionalMode', 'with'); if (this.additionalMode === 'WITH') next = next.set('additionalMode', 'with');
@ -2020,15 +2025,17 @@ export class Geral implements OnInit, AfterViewInit, OnDestroy {
normalized.includes('BLOQUE') || normalized.includes('BLOQUE') ||
normalized.includes('PERDA') || normalized.includes('PERDA') ||
normalized.includes('ROUBO') || normalized.includes('ROUBO') ||
normalized.includes('FURTO'); normalized.includes('FURTO') ||
normalized.includes('PREATIV');
if (!hasBlockedToken) return null; if (!hasBlockedToken) return null;
if (normalized.includes('120')) return 'BLOQUEIO_120'; if (normalized.includes('120')) return 'BLOQUEIO_120';
if (normalized.includes('PREATIV')) return 'PRE_ATIVACAO';
if (normalized.includes('PERDA') || normalized.includes('ROUBO') || normalized.includes('FURTO')) { if (normalized.includes('PERDA') || normalized.includes('ROUBO') || normalized.includes('FURTO')) {
return 'PERDA_ROUBO'; return 'PERDA_ROUBO';
} }
return 'PERDA_ROUBO'; return 'PRE_ATIVACAO';
} }
private isBlockedStatus(status: unknown): boolean { private isBlockedStatus(status: unknown): boolean {
@ -2939,6 +2946,7 @@ export class Geral implements OnInit, AfterViewInit, OnDestroy {
} else if (this.filterStatus === 'BLOCKED') { } else if (this.filterStatus === 'BLOCKED') {
if (this.blockedStatusMode === 'PERDA_ROUBO') parts.push('bloq-perda-roubo'); if (this.blockedStatusMode === 'PERDA_ROUBO') parts.push('bloq-perda-roubo');
else if (this.blockedStatusMode === 'BLOQUEIO_120') parts.push('bloq-120'); else if (this.blockedStatusMode === 'BLOQUEIO_120') parts.push('bloq-120');
else if (this.blockedStatusMode === 'PRE_ATIVACAO') parts.push('bloq-pre-ativacao');
else parts.push('bloqueadas'); else parts.push('bloqueadas');
} }