Compare commits

..

No commits in common. "ccea2fe13bd0a6025858e5afdf2a52df558acdc5" and "0d7186ce598cd5e9c2188e349b3be0c715effcd7" have entirely different histories.

27 changed files with 156 additions and 1130 deletions

View File

@ -28,11 +28,6 @@
{ {
"glob": "**/*", "glob": "**/*",
"input": "public" "input": "public"
},
{
"glob": "exceljs.min.js",
"input": "node_modules/exceljs/dist",
"output": "/vendor"
} }
], ],
"styles": [ "styles": [
@ -104,11 +99,6 @@
{ {
"glob": "**/*", "glob": "**/*",
"input": "public" "input": "public"
},
{
"glob": "exceljs.min.js",
"input": "node_modules/exceljs/dist",
"output": "/vendor"
} }
], ],
"styles": [ "styles": [

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 app-select-chevron"></i> <i class="bi bi-chevron-down"></i>
</button> </button>
<div class="app-select-panel" *ngIf="isOpen"> <div class="app-select-panel" *ngIf="isOpen">

View File

@ -75,7 +75,8 @@
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

@ -1,56 +0,0 @@
import { Component } from '@angular/core';
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { By } from '@angular/platform-browser';
import { FormsModule } from '@angular/forms';
import { CustomSelectComponent } from './custom-select';
@Component({
standalone: true,
imports: [FormsModule, CustomSelectComponent],
template: `
<app-select [options]="options" [(ngModel)]="firstValue"></app-select>
<app-select [options]="options" [(ngModel)]="secondValue"></app-select>
`,
})
class HostComponent {
options = [
{ label: 'Primeira', value: 'one' },
{ label: 'Segunda', value: 'two' },
];
firstValue = 'one';
secondValue = 'two';
}
describe('CustomSelectComponent', () => {
let fixture: ComponentFixture<HostComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [HostComponent],
}).compileComponents();
fixture = TestBed.createComponent(HostComponent);
fixture.detectChanges();
});
it('should keep only one select panel open at a time', () => {
const selectComponents = fixture.debugElement
.queryAll(By.directive(CustomSelectComponent))
.map((debugEl) => debugEl.componentInstance as CustomSelectComponent);
const triggerButtons = fixture.nativeElement.querySelectorAll('.app-select-trigger') as NodeListOf<HTMLButtonElement>;
triggerButtons[0].click();
fixture.detectChanges();
expect(selectComponents[0].isOpen).toBeTrue();
expect(selectComponents[1].isOpen).toBeFalse();
triggerButtons[1].click();
fixture.detectChanges();
expect(selectComponents[0].isOpen).toBeFalse();
expect(selectComponents[1].isOpen).toBeTrue();
expect(fixture.nativeElement.querySelectorAll('.app-select-panel').length).toBe(1);
});
});

View File

@ -1,8 +1,6 @@
import { Component, ElementRef, HostListener, Input, OnDestroy, forwardRef } from '@angular/core'; import { Component, ElementRef, HostListener, Input, forwardRef } from '@angular/core';
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'; import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
import { Subscription } from 'rxjs';
import { DropdownCoordinatorService } from '../../services/dropdown-coordinator.service';
@Component({ @Component({
selector: 'app-select', selector: 'app-select',
@ -18,9 +16,7 @@ import { DropdownCoordinatorService } from '../../services/dropdown-coordinator.
}, },
], ],
}) })
export class CustomSelectComponent implements ControlValueAccessor, OnDestroy { export class CustomSelectComponent implements ControlValueAccessor {
private static nextDropdownId = 0;
@Input() options: any[] = []; @Input() options: any[] = [];
@Input() placeholder = 'Selecione uma opção'; @Input() placeholder = 'Selecione uma opção';
@Input() labelKey = 'label'; @Input() labelKey = 'label';
@ -33,22 +29,11 @@ export class CustomSelectComponent implements ControlValueAccessor, OnDestroy {
isOpen = false; isOpen = false;
value: any = null; value: any = null;
searchTerm = ''; searchTerm = '';
private readonly dropdownId = `custom-select-${CustomSelectComponent.nextDropdownId++}`;
private readonly dropdownSyncSub: Subscription;
private onChange: (value: any) => void = () => {}; private onChange: (value: any) => void = () => {};
private onTouched: () => void = () => {}; private onTouched: () => void = () => {};
constructor( constructor(private host: ElementRef<HTMLElement>) {}
private host: ElementRef<HTMLElement>,
private dropdownCoordinator: DropdownCoordinatorService
) {
this.dropdownSyncSub = this.dropdownCoordinator.activeDropdownId$.subscribe((activeId) => {
if (this.isOpen && activeId !== this.dropdownId) {
this.close(false);
}
});
}
writeValue(value: any): void { writeValue(value: any): void {
this.value = value; this.value = value;
@ -64,7 +49,7 @@ export class CustomSelectComponent implements ControlValueAccessor, OnDestroy {
setDisabledState(isDisabled: boolean): void { setDisabledState(isDisabled: boolean): void {
this.disabled = isDisabled; this.disabled = isDisabled;
if (this.disabled) this.close(); if (this.disabled) this.isOpen = false;
} }
get displayLabel(): string { get displayLabel(): string {
@ -80,21 +65,13 @@ export class CustomSelectComponent implements ControlValueAccessor, OnDestroy {
toggle(): void { toggle(): void {
if (this.disabled) return; if (this.disabled) return;
if (this.isOpen) { this.isOpen = !this.isOpen;
this.close(); if (!this.isOpen) this.searchTerm = '';
return;
} }
this.dropdownCoordinator.requestOpen(this.dropdownId); close(): void {
this.isOpen = true;
}
close(notifyCoordinator = true): void {
this.isOpen = false; this.isOpen = false;
this.searchTerm = ''; this.searchTerm = '';
if (notifyCoordinator) {
this.dropdownCoordinator.clear(this.dropdownId);
}
} }
selectOption(option: any): void { selectOption(option: any): void {
@ -171,9 +148,4 @@ export class CustomSelectComponent implements ControlValueAccessor, OnDestroy {
onEsc(): void { onEsc(): void {
if (this.isOpen) this.close(); if (this.isOpen) this.close();
} }
ngOnDestroy(): void {
this.close();
this.dropdownSyncSub.unsubscribe();
}
} }

View File

@ -43,14 +43,6 @@
<span *ngIf="!exporting"><i class="bi bi-download me-1"></i> Exportar</span> <span *ngIf="!exporting"><i class="bi bi-download me-1"></i> Exportar</span>
<span *ngIf="exporting"><span class="spinner-border spinner-border-sm me-2"></span> Exportando...</span> <span *ngIf="exporting"><span class="spinner-border spinner-border-sm me-2"></span> Exportando...</span>
</button> </button>
<button
class="btn btn-glass btn-sm"
(click)="onExportTemplate()"
[disabled]="activeLoading || exportingTemplate"
>
<span *ngIf="!exportingTemplate"><i class="bi bi-file-earmark-arrow-down me-1"></i> Exportar Modelo</span>
<span *ngIf="exportingTemplate"><span class="spinner-border spinner-border-sm me-2"></span> Gerando Modelo...</span>
</button>
<button <button
*ngIf="isSysAdmin && activeTab === 'chips'" *ngIf="isSysAdmin && activeTab === 'chips'"
class="btn btn-brand btn-sm" class="btn btn-brand btn-sm"

View File

@ -7,7 +7,6 @@ import { CustomSelectComponent } from '../../components/custom-select/custom-sel
import { ChipsControleModalsComponent } from '../../components/page-modals/chips-controle-modals/chips-controle-modals'; import { ChipsControleModalsComponent } from '../../components/page-modals/chips-controle-modals/chips-controle-modals';
import { AuthService } from '../../services/auth.service'; import { AuthService } from '../../services/auth.service';
import { TableExportService } from '../../services/table-export.service'; import { TableExportService } from '../../services/table-export.service';
import { ImportPageTemplateService } from '../../services/import-page-template.service';
import { confirmDeletionWithTyping } from '../../utils/destructive-confirmation'; import { confirmDeletionWithTyping } from '../../utils/destructive-confirmation';
import { import {
buildPageNumbers, buildPageNumbers,
@ -99,7 +98,6 @@ export class ChipsControleRecebidos implements OnInit, OnDestroy {
toastMessage = ''; toastMessage = '';
toastType: 'success' | 'danger' = 'success'; toastType: 'success' | 'danger' = 'success';
exporting = false; exporting = false;
exportingTemplate = false;
private toastTimer: any = null; private toastTimer: any = null;
chipDetailOpen = false; chipDetailOpen = false;
@ -139,8 +137,7 @@ export class ChipsControleRecebidos implements OnInit, OnDestroy {
private service: ChipsControleService, private service: ChipsControleService,
private http: HttpClient, private http: HttpClient,
private authService: AuthService, private authService: AuthService,
private tableExportService: TableExportService, private tableExportService: TableExportService
private importPageTemplateService: ImportPageTemplateService
) {} ) {}
ngOnInit(): void { ngOnInit(): void {
@ -510,26 +507,6 @@ export class ChipsControleRecebidos implements OnInit, OnDestroy {
} }
} }
async onExportTemplate(): Promise<void> {
if (this.exportingTemplate) return;
this.exportingTemplate = true;
try {
if (this.activeTab === 'chips') {
await this.importPageTemplateService.exportChipsVirgensTemplate();
} else {
const selectedYear = typeof this.controleAno === 'number' ? this.controleAno : undefined;
await this.importPageTemplateService.exportControleRecebidosTemplate(selectedYear);
}
this.showToast('Modelo da aba exportado.', 'success');
} catch {
this.showToast('Erro ao exportar o modelo da aba.', 'danger');
} finally {
this.exportingTemplate = false;
}
}
private async fetchDetailedChipRowsForExport(rows: ChipVirgemListDto[]): Promise<ChipVirgemListDto[]> { private async fetchDetailedChipRowsForExport(rows: ChipVirgemListDto[]): Promise<ChipVirgemListDto[]> {
if (!rows.length) return []; if (!rows.length) return [];

View File

@ -36,10 +36,6 @@
<span *ngIf="!exporting"><i class="bi bi-download me-1"></i> Exportar</span> <span *ngIf="!exporting"><i class="bi bi-download me-1"></i> Exportar</span>
<span *ngIf="exporting"><span class="spinner-border spinner-border-sm me-2"></span> Exportando...</span> <span *ngIf="exporting"><span class="spinner-border spinner-border-sm me-2"></span> Exportando...</span>
</button> </button>
<button type="button" class="btn btn-glass btn-sm" (click)="onExportTemplate()" [disabled]="loading || exportingTemplate">
<span *ngIf="!exportingTemplate"><i class="bi bi-file-earmark-arrow-down me-1"></i> Exportar Modelo</span>
<span *ngIf="exportingTemplate"><span class="spinner-border spinner-border-sm me-2"></span> Gerando Modelo...</span>
</button>
<button *ngIf="isSysAdmin" type="button" class="btn btn-brand btn-sm" (click)="openCreate()"> <button *ngIf="isSysAdmin" type="button" class="btn btn-brand btn-sm" (click)="openCreate()">
<i class="bi bi-plus-circle me-1"></i> Novo Usuário <i class="bi bi-plus-circle me-1"></i> Novo Usuário
</button> </button>

View File

@ -6,7 +6,6 @@ import { firstValueFrom } from 'rxjs';
import { CustomSelectComponent } from '../../components/custom-select/custom-select'; import { CustomSelectComponent } from '../../components/custom-select/custom-select';
import { DadosUsuariosModalsComponent } from '../../components/page-modals/dados-usuarios-modals/dados-usuarios-modals'; import { DadosUsuariosModalsComponent } from '../../components/page-modals/dados-usuarios-modals/dados-usuarios-modals';
import { TableExportService } from '../../services/table-export.service'; import { TableExportService } from '../../services/table-export.service';
import { ImportPageTemplateService } from '../../services/import-page-template.service';
import { import {
DadosUsuariosService, DadosUsuariosService,
@ -58,7 +57,6 @@ export class DadosUsuarios implements OnInit {
loading = false; loading = false;
exporting = false; exporting = false;
exportingTemplate = false;
errorMsg = ''; errorMsg = '';
// Filtros // Filtros
@ -131,8 +129,7 @@ export class DadosUsuarios implements OnInit {
private service: DadosUsuariosService, private service: DadosUsuariosService,
private authService: AuthService, private authService: AuthService,
private linesService: LinesService, private linesService: LinesService,
private tableExportService: TableExportService, private tableExportService: TableExportService
private importPageTemplateService: ImportPageTemplateService
) {} ) {}
ngOnInit(): void { ngOnInit(): void {
@ -312,20 +309,6 @@ export class DadosUsuarios implements OnInit {
} }
} }
async onExportTemplate(): Promise<void> {
if (this.exportingTemplate) return;
this.exportingTemplate = true;
try {
await this.importPageTemplateService.exportDadosUsuariosTemplate(this.tipoFilter);
this.showToast('Modelo da página exportado.', 'success');
} catch {
this.showToast('Erro ao exportar o modelo da página.', 'danger');
} finally {
this.exportingTemplate = false;
}
}
private async fetchAllRowsForExport(): Promise<UserDataRow[]> { private async fetchAllRowsForExport(): Promise<UserDataRow[]> {
const pageSize = 500; const pageSize = 500;
let page = 1; let page = 1;

View File

@ -683,7 +683,7 @@ export class Dashboard implements OnInit, AfterViewInit, OnDestroy {
} }
private async fetchAllDashboardLines(onlyReserva: boolean): Promise<DashboardLineListItemDto[]> { private async fetchAllDashboardLines(onlyReserva: boolean): Promise<DashboardLineListItemDto[]> {
const pageSize = 5000; const pageSize = 500;
let page = 1; let page = 1;
const all: DashboardLineListItemDto[] = []; const all: DashboardLineListItemDto[] = [];

View File

@ -38,10 +38,6 @@
<span *ngIf="!exporting"><i class="bi bi-download me-1"></i> Exportar</span> <span *ngIf="!exporting"><i class="bi bi-download me-1"></i> Exportar</span>
<span *ngIf="exporting"><span class="spinner-border spinner-border-sm me-2"></span> Exportando...</span> <span *ngIf="exporting"><span class="spinner-border spinner-border-sm me-2"></span> Exportando...</span>
</button> </button>
<button type="button" class="btn btn-glass btn-sm" (click)="onExportTemplate()" [disabled]="loading || exportingTemplate">
<span *ngIf="!exportingTemplate"><i class="bi bi-file-earmark-arrow-down me-1"></i> Exportar Modelo</span>
<span *ngIf="exportingTemplate"><span class="spinner-border spinner-border-sm me-2"></span> Gerando Modelo...</span>
</button>
</div> </div>
</div> </div>

View File

@ -27,7 +27,6 @@ import {
import { AuthService } from '../../services/auth.service'; import { AuthService } from '../../services/auth.service';
import { LinesService } from '../../services/lines.service'; import { LinesService } from '../../services/lines.service';
import { TableExportService } from '../../services/table-export.service'; import { TableExportService } from '../../services/table-export.service';
import { ImportPageTemplateService } from '../../services/import-page-template.service';
import { confirmDeletionWithTyping } from '../../utils/destructive-confirmation'; import { confirmDeletionWithTyping } from '../../utils/destructive-confirmation';
import { import {
buildPageNumbers, buildPageNumbers,
@ -68,13 +67,11 @@ export class Faturamento implements AfterViewInit, OnDestroy {
private linesService: LinesService, private linesService: LinesService,
private cdr: ChangeDetectorRef, private cdr: ChangeDetectorRef,
private authService: AuthService, private authService: AuthService,
private tableExportService: TableExportService, private tableExportService: TableExportService
private importPageTemplateService: ImportPageTemplateService
) {} ) {}
loading = false; loading = false;
exporting = false; exporting = false;
exportingTemplate = false;
// filtros // filtros
searchTerm = ''; searchTerm = '';
@ -468,20 +465,6 @@ export class Faturamento implements AfterViewInit, OnDestroy {
} }
} }
async onExportTemplate(): Promise<void> {
if (this.exportingTemplate) return;
this.exportingTemplate = true;
try {
await this.importPageTemplateService.exportFaturamentoTemplate(this.filterTipo);
await this.showToast('Modelo da página exportado.');
} catch {
await this.showToast('Erro ao exportar o modelo da página.');
} finally {
this.exportingTemplate = false;
}
}
private getRowsForExport(): BillingItem[] { private getRowsForExport(): BillingItem[] {
const rows: BillingItem[] = []; const rows: BillingItem[] = [];
this.rowsByClient.forEach((items) => rows.push(...items)); this.rowsByClient.forEach((items) => rows.push(...items));

View File

@ -31,11 +31,11 @@
<small class="subtitle">Tabela de linhas e dados de telefonia</small> <small class="subtitle">Tabela de linhas e dados de telefonia</small>
</div> </div>
<div class="header-actions" data-animate> <div class="header-actions d-flex gap-2 justify-content-end align-items-start" data-animate>
<div class="header-actions-stack" *ngIf="isSysAdmin; else exportOnlyTpl"> <div class="d-flex flex-column gap-2 align-items-start" *ngIf="isSysAdmin; else exportOnlyTpl">
<button <button
type="button" type="button"
class="btn btn-glass btn-sm header-action-btn header-action-btn-wide" class="btn btn-glass btn-sm"
(click)="onImportExcel()" (click)="onImportExcel()"
[disabled]="loading"> [disabled]="loading">
<i class="bi bi-file-earmark-excel me-1"></i> Importar Dados Excel <i class="bi bi-file-earmark-excel me-1"></i> Importar Dados Excel
@ -43,7 +43,7 @@
<button <button
type="button" type="button"
class="btn btn-glass btn-sm header-action-btn header-action-btn-export" class="btn btn-glass btn-sm align-self-center"
(click)="onExport()" (click)="onExport()"
[disabled]="loading || exporting"> [disabled]="loading || exporting">
<span *ngIf="!exporting"><i class="bi bi-download me-1"></i> Exportar</span> <span *ngIf="!exporting"><i class="bi bi-download me-1"></i> Exportar</span>
@ -51,41 +51,28 @@
</button> </button>
</div> </div>
<ng-template #exportOnlyTpl> <ng-template #exportOnlyTpl>
<div class="header-actions-stack header-actions-stack-single">
<button <button
type="button" type="button"
class="btn btn-glass btn-sm header-action-btn header-action-btn-export" class="btn btn-glass btn-sm"
(click)="onExport()" (click)="onExport()"
[disabled]="loading || exporting"> [disabled]="loading || exporting">
<span *ngIf="!exporting"><i class="bi bi-download me-1"></i> Exportar</span> <span *ngIf="!exporting"><i class="bi bi-download me-1"></i> Exportar</span>
<span *ngIf="exporting"><span class="spinner-border spinner-border-sm me-2"></span> Exportando...</span> <span *ngIf="exporting"><span class="spinner-border spinner-border-sm me-2"></span> Exportando...</span>
</button> </button>
</div>
</ng-template> </ng-template>
<div class="header-actions-secondary"> <input #excelInput type="file" class="d-none" accept=".xlsx" (change)="onExcelSelected($event)" />
<button <input #batchExcelInput type="file" class="d-none" accept=".xlsx" (change)="onBatchExcelSelected($event)" />
type="button"
class="btn btn-glass btn-sm header-action-btn"
(click)="onExportTemplate()"
[disabled]="loading || exportingTemplate">
<span *ngIf="!exportingTemplate"><i class="bi bi-file-earmark-arrow-down me-1"></i> Exportar Modelo</span>
<span *ngIf="exportingTemplate"><span class="spinner-border spinner-border-sm me-2"></span> Gerando Modelo...</span>
</button>
<button <button
type="button" type="button"
class="btn btn-brand btn-sm header-action-btn" class="btn btn-brand btn-sm align-self-start"
*ngIf="canManageLines" *ngIf="canManageLines"
(click)="onCadastrarLinha()" (click)="onCadastrarLinha()"
[disabled]="loading"> [disabled]="loading">
<i class="bi bi-plus-circle me-1"></i> Novo Cliente <i class="bi bi-plus-circle me-1"></i> Novo Cliente
</button> </button>
</div> </div>
<input #excelInput type="file" class="d-none" accept=".xlsx" (change)="onExcelSelected($event)" />
<input #batchExcelInput type="file" class="d-none" accept=".xlsx" (change)="onBatchExcelSelected($event)" />
</div>
</div> </div>
<div class="filters-stack mt-4" data-animate> <div class="filters-stack mt-4" data-animate>
@ -111,18 +98,27 @@
<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>
<div class="filter-blocked-select-box"> <button type="button" class="filter-tab" [class.active]="filterStatus === 'BLOCKED'" (click)="toggleBlockedFilter()" [disabled]="loading">
<app-select <i class="bi bi-slash-circle me-1"></i> Bloqueadas
class="select-glass" </button>
size="sm" <ng-container *ngIf="filterStatus === 'BLOCKED'">
[options]="blockedStatusFilterOptions" <button
labelKey="label" type="button"
valueKey="value" class="filter-tab"
[ngModel]="blockedStatusSelectValue" [class.active]="blockedStatusMode === 'PERDA_ROUBO'"
(ngModelChange)="onBlockedStatusSelectChange($event)" (click)="setBlockedStatusMode('PERDA_ROUBO')"
[disabled]="loading" [disabled]="loading">
></app-select> Perda/Roubo
</div> </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>
</div> </div>
@ -351,7 +347,16 @@
</button> </button>
</div> </div>
<div class="controls-right"> <div class="page-size d-flex align-items-center gap-2">
<span class="text-muted small fw-bold text-uppercase" style="letter-spacing: 0.5px; font-size: 0.75rem;">
Itens por pág:
</span>
<div class="select-wrapper">
<app-select class="select-glass" size="sm" [options]="pageSizeOptions" [(ngModel)]="pageSize" (ngModelChange)="onPageSizeChange()" [disabled]="loading"></app-select>
</div>
</div>
<div class="batch-status-tools" *ngIf="canManageLines"> <div class="batch-status-tools" *ngIf="canManageLines">
<span class="batch-status-count"> <span class="batch-status-count">
Selecionadas: <strong>{{ batchStatusSelectionCount }}</strong> Selecionadas: <strong>{{ batchStatusSelectionCount }}</strong>
@ -371,17 +376,6 @@
<i class="bi bi-unlock me-1"></i> Desbloquear em lote <i class="bi bi-unlock me-1"></i> Desbloquear em lote
</button> </button>
</div> </div>
<div class="page-size d-flex align-items-center gap-2">
<span class="text-muted small fw-bold text-uppercase" style="letter-spacing: 0.5px; font-size: 0.75rem;">
Itens por pág:
</span>
<div class="select-wrapper">
<app-select class="select-glass" size="sm" [options]="pageSizeOptions" [(ngModel)]="pageSize" (ngModelChange)="onPageSizeChange()" [disabled]="loading"></app-select>
</div>
</div>
</div>
</div> </div>
</div> </div>

View File

@ -111,107 +111,12 @@
/* 3. HEADER, FILTROS E KPIs */ /* 3. HEADER, FILTROS E KPIs */
/* ========================================================== */ /* ========================================================== */
.geral-header { padding: 16px 24px; border-bottom: 1px solid rgba(17, 18, 20, 0.06); background: linear-gradient(180deg, rgba(227,61,207,0.06), rgba(255,255,255,0.2)); flex-shrink: 0; } .geral-header { padding: 16px 24px; border-bottom: 1px solid rgba(17, 18, 20, 0.06); background: linear-gradient(180deg, rgba(227,61,207,0.06), rgba(255,255,255,0.2)); flex-shrink: 0; }
.header-row-top { .header-row-top { display: grid; grid-template-columns: 1fr auto 1fr; align-items: center; gap: 12px; @media (max-width: 768px) { grid-template-columns: 1fr; text-align: center; gap: 16px; .title-badge { justify-self: center; margin-bottom: 8px; } .header-actions { justify-self: center; } } }
display: grid;
grid-template-columns: minmax(0, 1fr) auto minmax(0, 1fr);
align-items: center;
gap: 12px;
@media (max-width: 1366px) {
grid-template-columns: 1fr;
text-align: center;
gap: 16px;
.title-badge {
justify-self: center;
margin-bottom: 8px;
}
.header-title {
justify-self: center;
align-items: center;
text-align: center;
}
.header-actions {
justify-self: center;
}
}
}
.title-badge { justify-self: start; display: inline-flex; align-items: center; gap: 10px; padding: 6px 12px; border-radius: 999px; background: rgba(255, 255, 255, 0.78); border: 1px solid rgba(227, 61, 207, 0.22); backdrop-filter: blur(10px); color: var(--text); font-size: 13px; font-weight: 800; i { color: var(--brand); } } .title-badge { justify-self: start; display: inline-flex; align-items: center; gap: 10px; padding: 6px 12px; border-radius: 999px; background: rgba(255, 255, 255, 0.78); border: 1px solid rgba(227, 61, 207, 0.22); backdrop-filter: blur(10px); color: var(--text); font-size: 13px; font-weight: 800; i { color: var(--brand); } }
.header-title { justify-self: center; display: flex; flex-direction: column; align-items: center; text-align: center; } .header-title { justify-self: center; display: flex; flex-direction: column; align-items: center; text-align: center; }
.title { font-size: 26px; font-weight: 950; letter-spacing: -0.3px; color: var(--text); margin-top: 10px; margin-bottom: 0; } .title { font-size: 26px; font-weight: 950; letter-spacing: -0.3px; color: var(--text); margin-top: 10px; margin-bottom: 0; }
.subtitle { color: rgba(17, 18, 20, 0.65); font-weight: 700; } .subtitle { color: rgba(17, 18, 20, 0.65); font-weight: 700; }
.header-actions { .header-actions { justify-self: end; }
justify-self: end;
display: flex;
justify-content: flex-end;
align-items: flex-start;
gap: 12px;
flex-wrap: wrap;
max-width: 100%;
}
.header-actions-stack,
.header-actions-secondary {
display: flex;
flex-direction: column;
gap: 8px;
}
.header-actions-stack {
align-items: stretch;
min-width: 220px;
}
.header-actions-stack-single {
min-width: auto;
}
.header-actions-secondary {
align-items: stretch;
}
.header-action-btn {
white-space: nowrap;
}
.header-action-btn-wide {
min-width: 220px;
}
.header-action-btn-export {
align-self: center;
min-width: 140px;
}
@media (max-width: 1366px) {
.header-actions {
justify-content: center;
align-items: stretch;
}
}
@media (max-width: 920px) {
.header-actions {
flex-direction: column;
align-items: center;
width: 100%;
}
.header-actions-stack,
.header-actions-secondary,
.header-actions-stack-single {
width: min(100%, 280px);
}
.header-action-btn,
.header-action-btn-wide,
.header-action-btn-export {
width: 100%;
min-width: 0;
}
}
/* Botões */ /* Botões */
.btn-brand { background-color: var(--brand); border-color: var(--brand); color: #fff; font-weight: 900; border-radius: 12px; transition: transform 0.2s, box-shadow 0.2s; &:hover { transform: translateY(-2px); box-shadow: 0 10px 20px rgba(227, 61, 207, 0.25); filter: brightness(1.05); } &:disabled { opacity: 0.7; cursor: not-allowed; transform: none; } } .btn-brand { background-color: var(--brand); border-color: var(--brand); color: #fff; font-weight: 900; border-radius: 12px; transition: transform 0.2s, box-shadow 0.2s; &:hover { transform: translateY(-2px); box-shadow: 0 10px 20px rgba(227, 61, 207, 0.25); filter: brightness(1.05); } &:disabled { opacity: 0.7; cursor: not-allowed; transform: none; } }
@ -238,45 +143,13 @@
.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; }
@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); } }
@ -462,32 +335,17 @@
/* 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 { @media (max-width: 500px) { width: 100%; justify-content: space-between; } } .page-size { margin-left: auto; @media (max-width: 500px) { margin-left: 0; width: 100%; justify-content: space-between; } }
.batch-status-tools { .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

@ -5,7 +5,6 @@ import { HttpParams } from '@angular/common/http';
import { provideRouter } from '@angular/router'; import { provideRouter } from '@angular/router';
import { Geral } from './geral'; import { Geral } from './geral';
import { DropdownCoordinatorService } from '../../services/dropdown-coordinator.service';
describe('Geral', () => { describe('Geral', () => {
let component: Geral; let component: Geral;
@ -144,23 +143,6 @@ 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',
@ -177,33 +159,6 @@ 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());
@ -216,20 +171,4 @@ describe('Geral', () => {
expect(params.get('includeAssignedReservaInAll')).toBeNull(); expect(params.get('includeAssignedReservaInAll')).toBeNull();
expect(params.get('skil')).toBe('RESERVA'); expect(params.get('skil')).toBe('RESERVA');
}); });
it('should close custom filter dropdowns when another filter dropdown opens', () => {
const dropdownCoordinator = TestBed.inject(DropdownCoordinatorService);
component.toggleAdditionalMenu();
expect(component.showAdditionalMenu).toBeTrue();
component.toggleClientMenu();
expect(component.showClientMenu).toBeTrue();
expect(component.showAdditionalMenu).toBeFalse();
dropdownCoordinator.requestOpen('external-filter');
expect(component.showClientMenu).toBeFalse();
expect(component.showAdditionalMenu).toBeFalse();
});
}); });

View File

@ -24,7 +24,6 @@ import { PlanAutoFillService } from '../../services/plan-autofill.service';
import { AuthService } from '../../services/auth.service'; import { AuthService } from '../../services/auth.service';
import { TenantSyncService } from '../../services/tenant-sync.service'; import { TenantSyncService } from '../../services/tenant-sync.service';
import { TableExportService } from '../../services/table-export.service'; import { TableExportService } from '../../services/table-export.service';
import { ImportPageTemplateService } from '../../services/import-page-template.service';
import { SolicitacoesLinhasService } from '../../services/solicitacoes-linhas.service'; import { SolicitacoesLinhasService } from '../../services/solicitacoes-linhas.service';
import { import {
MveAuditService, MveAuditService,
@ -32,7 +31,6 @@ import {
type MveAuditIssue, type MveAuditIssue,
type MveAuditRun, type MveAuditRun,
} from '../../services/mve-audit.service'; } from '../../services/mve-audit.service';
import { DropdownCoordinatorService } from '../../services/dropdown-coordinator.service';
import { firstValueFrom, Subscription, filter } from 'rxjs'; import { firstValueFrom, Subscription, filter } from 'rxjs';
import { environment } from '../../../environments/environment'; import { environment } from '../../../environments/environment';
import { confirmDeletionWithTyping } from '../../utils/destructive-confirmation'; import { confirmDeletionWithTyping } from '../../utils/destructive-confirmation';
@ -69,8 +67,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' | 'PRE_ATIVACAO'; type BlockedStatusMode = 'ALL' | 'PERDA_ROUBO' | 'BLOQUEIO_120';
type BlockedStatusFilterValue = '' | BlockedStatusMode;
type SkilFilterMode = 'ALL' | 'PF' | 'PJ' | 'RESERVA' | 'ESTOQUE'; type SkilFilterMode = 'ALL' | 'PF' | 'PJ' | 'RESERVA' | 'ESTOQUE';
interface LineRow { interface LineRow {
@ -364,14 +361,9 @@ interface MveApplySelectionSummary {
styleUrls: ['./geral.scss'] styleUrls: ['./geral.scss']
}) })
export class Geral implements OnInit, AfterViewInit, OnDestroy { export class Geral implements OnInit, AfterViewInit, OnDestroy {
private static nextFilterDropdownScopeId = 0;
readonly vm = this; readonly vm = this;
readonly batchMassColumnGuide = BATCH_MASS_COLUMN_GUIDE; readonly batchMassColumnGuide = BATCH_MASS_COLUMN_GUIDE;
toastMessage = ''; toastMessage = '';
private readonly filterDropdownScopeId = Geral.nextFilterDropdownScopeId++;
private readonly clientDropdownId = `geral-client-filter-${this.filterDropdownScopeId}`;
private readonly additionalDropdownId = `geral-additional-filter-${this.filterDropdownScopeId}`;
@ViewChild('successToast', { static: false }) successToast!: ElementRef; @ViewChild('successToast', { static: false }) successToast!: ElementRef;
@ViewChild('excelInput') excelInput!: ElementRef<HTMLInputElement>; @ViewChild('excelInput') excelInput!: ElementRef<HTMLInputElement>;
@ -393,16 +385,13 @@ export class Geral implements OnInit, AfterViewInit, OnDestroy {
private tenantSyncService: TenantSyncService, private tenantSyncService: TenantSyncService,
private solicitacoesLinhasService: SolicitacoesLinhasService, private solicitacoesLinhasService: SolicitacoesLinhasService,
private tableExportService: TableExportService, private tableExportService: TableExportService,
private importPageTemplateService: ImportPageTemplateService, private mveAuditService: MveAuditService
private mveAuditService: MveAuditService,
private dropdownCoordinator: DropdownCoordinatorService
) {} ) {}
private readonly apiBase = buildApiEndpoint(environment.apiUrl, 'lines'); private readonly apiBase = buildApiEndpoint(environment.apiUrl, 'lines');
private readonly templatesApiBase = buildApiEndpoint(environment.apiUrl, 'templates'); private readonly templatesApiBase = buildApiEndpoint(environment.apiUrl, 'templates');
loading = false; loading = false;
exporting = false; exporting = false;
exportingTemplate = false;
isSysAdmin = false; isSysAdmin = false;
isGestor = false; isGestor = false;
isFinanceiro = false; isFinanceiro = false;
@ -422,13 +411,6 @@ 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: BlockedStatusFilterValue }> = [
{ label: 'Todos os status', value: '' },
{ 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' },
@ -527,7 +509,6 @@ export class Geral implements OnInit, AfterViewInit, OnDestroy {
private editingId: string | null = null; private editingId: string | null = null;
private searchTimer: any = null; private searchTimer: any = null;
private navigationSub?: Subscription; private navigationSub?: Subscription;
private dropdownSyncSub?: Subscription;
private keepPageOnNextGroupsLoad = false; private keepPageOnNextGroupsLoad = false;
private searchResolvedClient: string | null = null; private searchResolvedClient: string | null = null;
@ -544,7 +525,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', 'BLOQUEIO DE PRÉ ATIVAÇÃO']; readonly statusOptions = ['ATIVO', 'BLOQUEIO PERDA/ROUBO', 'BLOQUEIO 120 DIAS'];
readonly skilOptions = ['PESSOA FÍSICA', 'PESSOA JURÍDICA', 'RESERVA']; readonly skilOptions = ['PESSOA FÍSICA', 'PESSOA JURÍDICA', 'RESERVA'];
planOptions = [ planOptions = [
@ -727,10 +708,6 @@ export class Geral implements OnInit, AfterViewInit, OnDestroy {
return (this.batchExcelPreview?.rows ?? []).slice(0, 8); return (this.batchExcelPreview?.rows ?? []).slice(0, 8);
} }
get blockedStatusSelectValue(): BlockedStatusFilterValue {
return this.filterStatus === 'BLOCKED' ? this.blockedStatusMode : '';
}
getBatchExcelRowErrorsTitle(row: BatchExcelPreviewRowDto | null | undefined): string { getBatchExcelRowErrorsTitle(row: BatchExcelPreviewRowDto | null | undefined): string {
const errors = row?.errors ?? []; const errors = row?.errors ?? [];
return errors return errors
@ -991,12 +968,12 @@ export class Geral implements OnInit, AfterViewInit, OnDestroy {
let changed = false; let changed = false;
if (this.showClientMenu && !insideClient) { if (this.showClientMenu && !insideClient) {
this.closeClientDropdown(); this.showClientMenu = false;
changed = true; changed = true;
} }
if (this.showAdditionalMenu && !insideAdditional) { if (this.showAdditionalMenu && !insideAdditional) {
this.closeAdditionalDropdown(); this.showAdditionalMenu = false;
changed = true; changed = true;
} }
@ -1022,12 +999,12 @@ export class Geral implements OnInit, AfterViewInit, OnDestroy {
let changed = false; let changed = false;
if (this.showClientMenu) { if (this.showClientMenu) {
this.closeClientDropdown(); this.showClientMenu = false;
changed = true; changed = true;
} }
if (this.showAdditionalMenu) { if (this.showAdditionalMenu) {
this.closeAdditionalDropdown(); this.showAdditionalMenu = false;
changed = true; changed = true;
} }
@ -1041,7 +1018,6 @@ export class Geral implements OnInit, AfterViewInit, OnDestroy {
ngOnDestroy(): void { ngOnDestroy(): void {
if (this.searchTimer) clearTimeout(this.searchTimer); if (this.searchTimer) clearTimeout(this.searchTimer);
this.navigationSub?.unsubscribe(); this.navigationSub?.unsubscribe();
this.dropdownSyncSub?.unsubscribe();
} }
ngOnInit(): void { ngOnInit(): void {
@ -1059,16 +1035,6 @@ export class Geral implements OnInit, AfterViewInit, OnDestroy {
this.selectedAdditionalServices = []; this.selectedAdditionalServices = [];
this.selectedClients = []; this.selectedClients = [];
} }
this.dropdownSyncSub = this.dropdownCoordinator.activeDropdownId$.subscribe((activeId) => {
if (activeId !== this.clientDropdownId) {
this.closeClientDropdown(false);
}
if (activeId !== this.additionalDropdownId) {
this.closeAdditionalDropdown(false);
}
});
} }
async ngAfterViewInit() { async ngAfterViewInit() {
@ -1229,13 +1195,6 @@ 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;
} }
@ -1867,37 +1826,13 @@ export class Geral implements OnInit, AfterViewInit, OnDestroy {
this.refreshData(); this.refreshData();
} }
setBlockedStatusFilter(mode: BlockedStatusFilterValue) { toggleBlockedFilter() {
const normalizedMode = mode ?? ''; if (this.filterStatus === 'BLOCKED') {
if (normalizedMode === '') {
this.filterStatus = 'ALL';
this.blockedStatusMode = 'ALL';
this.expandedGroup = null;
this.groupLines = [];
this.searchResolvedClient = null;
this.selectedClients = [];
this.clientSearchTerm = '';
this.page = 1;
if (!this.isClientRestricted) {
this.loadClients();
}
this.refreshData();
return;
}
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;
@ -1912,8 +1847,24 @@ export class Geral implements OnInit, AfterViewInit, OnDestroy {
this.refreshData(); this.refreshData();
} }
onBlockedStatusSelectChange(value: BlockedStatusFilterValue) { setBlockedStatusMode(mode: Exclude<BlockedStatusMode, 'ALL'>) {
this.setBlockedStatusFilter(value); 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) {
@ -2004,7 +1955,6 @@ 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');
@ -2051,17 +2001,15 @@ 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 'PRE_ATIVACAO'; return 'PERDA_ROUBO';
} }
private isBlockedStatus(status: unknown): boolean { private isBlockedStatus(status: unknown): boolean {
@ -2588,43 +2536,27 @@ export class Geral implements OnInit, AfterViewInit, OnDestroy {
toggleClientMenu() { toggleClientMenu() {
if (this.isClientRestricted) return; if (this.isClientRestricted) return;
if (this.showClientMenu) { if (!this.showClientMenu) this.showAdditionalMenu = false;
this.closeClientDropdown(); this.showClientMenu = !this.showClientMenu;
return;
}
this.dropdownCoordinator.requestOpen(this.clientDropdownId);
this.showClientMenu = true;
} }
toggleAdditionalMenu() { toggleAdditionalMenu() {
if (this.isClientRestricted) return; if (this.isClientRestricted) return;
if (this.showAdditionalMenu) { if (!this.showAdditionalMenu) this.showClientMenu = false;
this.closeAdditionalDropdown(); this.showAdditionalMenu = !this.showAdditionalMenu;
return;
} }
this.dropdownCoordinator.requestOpen(this.additionalDropdownId); closeClientDropdown() {
this.showAdditionalMenu = true;
}
closeClientDropdown(notifyCoordinator = true) {
this.showClientMenu = false; this.showClientMenu = false;
if (notifyCoordinator) {
this.dropdownCoordinator.clear(this.clientDropdownId);
}
} }
closeAdditionalDropdown(notifyCoordinator = true) { closeAdditionalDropdown() {
this.showAdditionalMenu = false; this.showAdditionalMenu = false;
if (notifyCoordinator) {
this.dropdownCoordinator.clear(this.additionalDropdownId);
}
} }
closeFilterDropdowns() { closeFilterDropdowns() {
this.closeClientDropdown(); this.showClientMenu = false;
this.closeAdditionalDropdown(); this.showAdditionalMenu = false;
} }
selectClient(client: string | null) { selectClient(client: string | null) {
@ -2827,20 +2759,6 @@ export class Geral implements OnInit, AfterViewInit, OnDestroy {
} }
} }
async onExportTemplate(): Promise<void> {
if (this.exportingTemplate) return;
this.exportingTemplate = true;
try {
await this.importPageTemplateService.exportGeralTemplate();
await this.showToast('Modelo da página exportado.');
} catch {
await this.showToast('Erro ao exportar o modelo da página.');
} finally {
this.exportingTemplate = false;
}
}
private async getRowsForExport(): Promise<LineRow[]> { private async getRowsForExport(): Promise<LineRow[]> {
let lines = await this.fetchLinesForGrouping(); let lines = await this.fetchLinesForGrouping();
@ -2986,7 +2904,6 @@ 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');
} }

View File

@ -35,10 +35,6 @@
<span *ngIf="!exporting"><i class="bi bi-download me-1"></i> Exportar</span> <span *ngIf="!exporting"><i class="bi bi-download me-1"></i> Exportar</span>
<span *ngIf="exporting"><span class="spinner-border spinner-border-sm me-2"></span> Exportando...</span> <span *ngIf="exporting"><span class="spinner-border spinner-border-sm me-2"></span> Exportando...</span>
</button> </button>
<button type="button" class="btn btn-glass btn-sm" (click)="onExportTemplate()" [disabled]="loading || exportingTemplate">
<span *ngIf="!exportingTemplate"><i class="bi bi-file-earmark-arrow-down me-1"></i> Exportar Modelo</span>
<span *ngIf="exportingTemplate"><span class="spinner-border spinner-border-sm me-2"></span> Gerando Modelo...</span>
</button>
<button *ngIf="canManageRecords" type="button" class="btn btn-brand btn-sm" (click)="onCreate()" [disabled]="loading"> <button *ngIf="canManageRecords" type="button" class="btn btn-brand btn-sm" (click)="onCreate()" [disabled]="loading">
<i class="bi bi-plus-circle me-1"></i> Nova Mureg <i class="bi bi-plus-circle me-1"></i> Nova Mureg
</button> </button>

View File

@ -15,7 +15,6 @@ import { AuthService } from '../../services/auth.service';
import { LinesService } from '../../services/lines.service'; import { LinesService } from '../../services/lines.service';
import { CustomSelectComponent } from '../../components/custom-select/custom-select'; import { CustomSelectComponent } from '../../components/custom-select/custom-select';
import { TableExportService } from '../../services/table-export.service'; import { TableExportService } from '../../services/table-export.service';
import { ImportPageTemplateService } from '../../services/import-page-template.service';
import { environment } from '../../../environments/environment'; import { environment } from '../../../environments/environment';
import { confirmDeletionWithTyping } from '../../utils/destructive-confirmation'; import { confirmDeletionWithTyping } from '../../utils/destructive-confirmation';
import { MuregModalsComponent } from '../../components/page-modals/mureg-modals/mureg-modals'; import { MuregModalsComponent } from '../../components/page-modals/mureg-modals/mureg-modals';
@ -110,7 +109,6 @@ export class Mureg implements AfterViewInit {
toastMessage = ''; toastMessage = '';
loading = false; loading = false;
exporting = false; exporting = false;
exportingTemplate = false;
@ViewChild('successToast', { static: false }) successToast!: ElementRef; @ViewChild('successToast', { static: false }) successToast!: ElementRef;
@ -120,8 +118,7 @@ export class Mureg implements AfterViewInit {
private cdr: ChangeDetectorRef, private cdr: ChangeDetectorRef,
private authService: AuthService, private authService: AuthService,
private linesService: LinesService, private linesService: LinesService,
private tableExportService: TableExportService, private tableExportService: TableExportService
private importPageTemplateService: ImportPageTemplateService
) {} ) {}
private readonly apiBase = buildApiEndpoint(environment.apiUrl, 'mureg'); private readonly apiBase = buildApiEndpoint(environment.apiUrl, 'mureg');
@ -267,20 +264,6 @@ export class Mureg implements AfterViewInit {
} }
} }
async onExportTemplate(): Promise<void> {
if (this.exportingTemplate) return;
this.exportingTemplate = true;
try {
await this.importPageTemplateService.exportMuregTemplate();
await this.showToast('Modelo da página exportado.');
} catch {
await this.showToast('Erro ao exportar o modelo da página.');
} finally {
this.exportingTemplate = false;
}
}
private async fetchAllRowsForExport(): Promise<MuregRow[]> { private async fetchAllRowsForExport(): Promise<MuregRow[]> {
const pageSize = 2000; const pageSize = 2000;
let page = 1; let page = 1;

View File

@ -29,10 +29,6 @@
<span *ngIf="!exporting"><i class="bi bi-download"></i> Exportar</span> <span *ngIf="!exporting"><i class="bi bi-download"></i> Exportar</span>
<span *ngIf="exporting"><span class="spinner-border spinner-border-sm me-2"></span> Exportando...</span> <span *ngIf="exporting"><span class="spinner-border spinner-border-sm me-2"></span> Exportando...</span>
</button> </button>
<button class="btn-ghost btn-export-glass" type="button" (click)="onExportTemplate()" [disabled]="loading || exportingTemplate">
<span *ngIf="!exportingTemplate"><i class="bi bi-file-earmark-arrow-down"></i> Exportar Modelo</span>
<span *ngIf="exportingTemplate"><span class="spinner-border spinner-border-sm me-2"></span> Gerando Modelo...</span>
</button>
<button *ngIf="canManageRecords" class="btn-primary" type="button" (click)="openCreateModal()"> <button *ngIf="canManageRecords" class="btn-primary" type="button" (click)="openCreateModal()">
<i class="bi bi-plus-circle"></i> Novo Parcelamento <i class="bi bi-plus-circle"></i> Novo Parcelamento
</button> </button>

View File

@ -6,7 +6,6 @@ import { finalize, Subscription, firstValueFrom, timeout } from 'rxjs';
import { ParcelamentosModalsComponent } from '../../components/page-modals/parcelamento-modals/parcelamentos-modals'; import { ParcelamentosModalsComponent } from '../../components/page-modals/parcelamento-modals/parcelamentos-modals';
import { AuthService } from '../../services/auth.service'; import { AuthService } from '../../services/auth.service';
import { TableExportService } from '../../services/table-export.service'; import { TableExportService } from '../../services/table-export.service';
import { ImportPageTemplateService } from '../../services/import-page-template.service';
import { import {
ParcelamentosService, ParcelamentosService,
ParcelamentoListItem, ParcelamentoListItem,
@ -69,7 +68,6 @@ export class Parcelamentos implements OnInit, OnDestroy {
readonly vm = this; readonly vm = this;
loading = false; loading = false;
exporting = false; exporting = false;
exportingTemplate = false;
errorMessage = ''; errorMessage = '';
toastOpen = false; toastOpen = false;
toastMessage = ''; toastMessage = '';
@ -162,8 +160,7 @@ export class Parcelamentos implements OnInit, OnDestroy {
constructor( constructor(
private parcelamentosService: ParcelamentosService, private parcelamentosService: ParcelamentosService,
private authService: AuthService, private authService: AuthService,
private tableExportService: TableExportService, private tableExportService: TableExportService
private importPageTemplateService: ImportPageTemplateService
) {} ) {}
ngOnInit(): void { ngOnInit(): void {
@ -338,20 +335,6 @@ export class Parcelamentos implements OnInit, OnDestroy {
} }
} }
async onExportTemplate(): Promise<void> {
if (this.exportingTemplate) return;
this.exportingTemplate = true;
try {
await this.importPageTemplateService.exportParcelamentosTemplate(this.resolveTemplateStartYear());
this.showToast('Modelo da página exportado.', 'success');
} catch {
this.showToast('Erro ao exportar o modelo da página.', 'danger');
} finally {
this.exportingTemplate = false;
}
}
onPageSizeChange(size: number): void { onPageSizeChange(size: number): void {
this.pageSize = size; this.pageSize = size;
this.page = 1; this.page = 1;
@ -1176,19 +1159,6 @@ export class Parcelamentos implements OnInit, OnDestroy {
return Number.isNaN(n) ? null : n; return Number.isNaN(n) ? null : n;
} }
private resolveTemplateStartYear(): number {
const filteredYear = this.parseNumber(this.filters.anoRef);
if (filteredYear && filteredYear > 0) {
return filteredYear;
}
const listedYear = (this.items ?? [])
.map((item) => this.parseNumber(item.anoRef))
.find((year): year is number => year !== null && year > 0);
return listedYear ?? new Date().getFullYear();
}
private toNumber(value: any): number | null { private toNumber(value: any): number | null {
if (value === null || value === undefined) return null; if (value === null || value === undefined) return null;
if (typeof value === 'number') return Number.isFinite(value) ? value : null; if (typeof value === 'number') return Number.isFinite(value) ? value : null;

View File

@ -35,10 +35,6 @@
<span *ngIf="!exporting"><i class="bi bi-download me-1"></i> Exportar</span> <span *ngIf="!exporting"><i class="bi bi-download me-1"></i> Exportar</span>
<span *ngIf="exporting"><span class="spinner-border spinner-border-sm me-2"></span> Exportando...</span> <span *ngIf="exporting"><span class="spinner-border spinner-border-sm me-2"></span> Exportando...</span>
</button> </button>
<button type="button" class="btn btn-glass btn-sm" (click)="onExportTemplate()" [disabled]="loading || exportingTemplate">
<span *ngIf="!exportingTemplate"><i class="bi bi-file-earmark-arrow-down me-1"></i> Exportar Modelo</span>
<span *ngIf="exportingTemplate"><span class="spinner-border spinner-border-sm me-2"></span> Gerando Modelo...</span>
</button>
<button *ngIf="canManageRecords" type="button" class="btn btn-brand btn-sm" (click)="onCreate()" [disabled]="loading"> <button *ngIf="canManageRecords" type="button" class="btn btn-brand btn-sm" (click)="onCreate()" [disabled]="loading">
<i class="bi bi-plus-circle me-1"></i> Nova Troca <i class="bi bi-plus-circle me-1"></i> Nova Troca
</button> </button>

View File

@ -14,7 +14,6 @@ import { firstValueFrom } from 'rxjs';
import { AuthService } from '../../services/auth.service'; import { AuthService } from '../../services/auth.service';
import { CustomSelectComponent } from '../../components/custom-select/custom-select'; import { CustomSelectComponent } from '../../components/custom-select/custom-select';
import { TableExportService } from '../../services/table-export.service'; import { TableExportService } from '../../services/table-export.service';
import { ImportPageTemplateService } from '../../services/import-page-template.service';
import { environment } from '../../../environments/environment'; import { environment } from '../../../environments/environment';
import { TrocaNumeroModalsComponent } from '../../components/page-modals/troca-numero-modals/troca-numero-modals'; import { TrocaNumeroModalsComponent } from '../../components/page-modals/troca-numero-modals/troca-numero-modals';
import { import {
@ -78,7 +77,6 @@ export class TrocaNumero implements AfterViewInit {
toastMessage = ''; toastMessage = '';
loading = false; loading = false;
exporting = false; exporting = false;
exportingTemplate = false;
@ViewChild('successToast', { static: false }) successToast!: ElementRef; @ViewChild('successToast', { static: false }) successToast!: ElementRef;
@ -87,8 +85,7 @@ export class TrocaNumero implements AfterViewInit {
private http: HttpClient, private http: HttpClient,
private cdr: ChangeDetectorRef, private cdr: ChangeDetectorRef,
private authService: AuthService, private authService: AuthService,
private tableExportService: TableExportService, private tableExportService: TableExportService
private importPageTemplateService: ImportPageTemplateService
) {} ) {}
private readonly apiBase = buildApiEndpoint(environment.apiUrl, 'trocanumero'); private readonly apiBase = buildApiEndpoint(environment.apiUrl, 'trocanumero');
@ -216,20 +213,6 @@ export class TrocaNumero implements AfterViewInit {
} }
} }
async onExportTemplate(): Promise<void> {
if (this.exportingTemplate) return;
this.exportingTemplate = true;
try {
await this.importPageTemplateService.exportTrocaNumeroTemplate();
await this.showToast('Modelo da página exportado.');
} catch {
await this.showToast('Erro ao exportar o modelo da página.');
} finally {
this.exportingTemplate = false;
}
}
private async fetchAllRowsForExport(): Promise<TrocaRow[]> { private async fetchAllRowsForExport(): Promise<TrocaRow[]> {
const pageSize = 2000; const pageSize = 2000;
let page = 1; let page = 1;

View File

@ -28,10 +28,6 @@
<span *ngIf="!exporting"><i class="bi bi-download me-1"></i> Exportar</span> <span *ngIf="!exporting"><i class="bi bi-download me-1"></i> Exportar</span>
<span *ngIf="exporting"><span class="spinner-border spinner-border-sm me-2"></span> Exportando...</span> <span *ngIf="exporting"><span class="spinner-border spinner-border-sm me-2"></span> Exportando...</span>
</button> </button>
<button class="btn btn-glass btn-sm" (click)="onExportTemplate()" [disabled]="loading || exportingTemplate">
<span *ngIf="!exportingTemplate"><i class="bi bi-file-earmark-arrow-down me-1"></i> Exportar Modelo</span>
<span *ngIf="exportingTemplate"><span class="spinner-border spinner-border-sm me-2"></span> Gerando Modelo...</span>
</button>
<button *ngIf="isSysAdmin" class="btn btn-brand btn-sm" (click)="openCreate()"> <button *ngIf="isSysAdmin" class="btn btn-brand btn-sm" (click)="openCreate()">
<i class="bi bi-plus-circle me-1"></i> Nova Vigência <i class="bi bi-plus-circle me-1"></i> Nova Vigência
</button> </button>

View File

@ -10,7 +10,6 @@ import { AuthService } from '../../services/auth.service';
import { LinesService, MobileLineDetail } from '../../services/lines.service'; import { LinesService, MobileLineDetail } from '../../services/lines.service';
import { PlanAutoFillService } from '../../services/plan-autofill.service'; import { PlanAutoFillService } from '../../services/plan-autofill.service';
import { TableExportService } from '../../services/table-export.service'; import { TableExportService } from '../../services/table-export.service';
import { ImportPageTemplateService } from '../../services/import-page-template.service';
import { confirmDeletionWithTyping } from '../../utils/destructive-confirmation'; import { confirmDeletionWithTyping } from '../../utils/destructive-confirmation';
import { computeTotalPages } from '../../utils/pagination.util'; import { computeTotalPages } from '../../utils/pagination.util';
import { VigenciaModalsComponent } from '../../components/page-modals/vigencia-modals/vigencia-modals'; import { VigenciaModalsComponent } from '../../components/page-modals/vigencia-modals/vigencia-modals';
@ -38,7 +37,6 @@ export class VigenciaComponent implements OnInit, OnDestroy {
readonly vm = this; readonly vm = this;
loading = false; loading = false;
exporting = false; exporting = false;
exportingTemplate = false;
errorMsg = ''; errorMsg = '';
// Filtros // Filtros
@ -121,8 +119,7 @@ export class VigenciaComponent implements OnInit, OnDestroy {
private linesService: LinesService, private linesService: LinesService,
private planAutoFill: PlanAutoFillService, private planAutoFill: PlanAutoFillService,
private route: ActivatedRoute, private route: ActivatedRoute,
private tableExportService: TableExportService, private tableExportService: TableExportService
private importPageTemplateService: ImportPageTemplateService
) {} ) {}
ngOnInit(): void { ngOnInit(): void {
@ -350,20 +347,6 @@ export class VigenciaComponent implements OnInit, OnDestroy {
} }
} }
async onExportTemplate(): Promise<void> {
if (this.exportingTemplate) return;
this.exportingTemplate = true;
try {
await this.importPageTemplateService.exportVigenciaTemplate();
this.showToast('Modelo da página exportado.', 'success');
} catch {
this.showToast('Erro ao exportar o modelo da página.', 'danger');
} finally {
this.exportingTemplate = false;
}
}
private async fetchAllRowsForExport(): Promise<VigenciaRow[]> { private async fetchAllRowsForExport(): Promise<VigenciaRow[]> {
const pageSize = 500; const pageSize = 500;
let page = 1; let page = 1;

View File

@ -1,25 +0,0 @@
import { Injectable } from '@angular/core';
import { BehaviorSubject } from 'rxjs';
@Injectable({
providedIn: 'root',
})
export class DropdownCoordinatorService {
private readonly activeDropdownIdSubject = new BehaviorSubject<string | null>(null);
readonly activeDropdownId$ = this.activeDropdownIdSubject.asObservable();
get activeDropdownId(): string | null {
return this.activeDropdownIdSubject.value;
}
requestOpen(id: string): void {
if (!id || this.activeDropdownIdSubject.value === id) return;
this.activeDropdownIdSubject.next(id);
}
clear(id?: string): void {
if (id && this.activeDropdownIdSubject.value !== id) return;
if (this.activeDropdownIdSubject.value === null) return;
this.activeDropdownIdSubject.next(null);
}
}

View File

@ -1,296 +0,0 @@
import { Injectable } from '@angular/core';
import { TableExportColumn, TableExportService } from './table-export.service';
type TemplateCell = string | number | boolean | Date | null | undefined;
type TemplateRow = Record<string, TemplateCell>;
type FaturamentoTemplateMode = 'ALL' | 'PF' | 'PJ';
type DadosUsuariosTemplateMode = 'ALL' | 'PF' | 'PJ';
interface TemplateExportConfig {
fileNameBase: string;
sheetName: string;
headerRows: TemplateCell[][];
}
@Injectable({ providedIn: 'root' })
export class ImportPageTemplateService {
private readonly parcelamentoMonths = ['JAN', 'FEV', 'MAR', 'ABR', 'MAI', 'JUN', 'JUL', 'AGO', 'SET', 'OUT', 'NOV', 'DEZ'] as const;
constructor(private readonly tableExportService: TableExportService) {}
exportGeralTemplate(): Promise<void> {
return this.exportTemplate({
fileNameBase: 'modelo_geral',
sheetName: 'GERAL',
headerRows: [[
'ITEM',
'CONTA',
'LINHA',
'CHIP',
'CLIENTE',
'USUÁRIO',
'PLANO CONTRATO',
'FRAQUIA',
'VALOR DO PLANO R$',
'GESTÃO VOZ E DADOS R$',
'SKEELO',
'VIVO NEWS PLUS',
'VIVO TRAVEL MUNDO',
'VIVO SYNC',
'VIVO GESTÃO DISPOSITIVO',
'VALOR CONTRATO VIVO',
'FRANQUIA LINE',
'FRANQUIA GESTÃO',
'LOCAÇÃO AP.',
'VALOR CONTRATO LINE',
'DESCONTO',
'LUCRO',
'STATUS',
'DATA DO BLOQUEIO',
'SKIL',
'MODALIDADE',
'CEDENTE',
'SOLICITANTE',
'DATA DA ENTREGA OPERA.',
'DATA DA ENTREGA CLIENTE',
'VENC. DA CONTA',
'TIPO DE CHIP',
]],
});
}
exportMuregTemplate(): Promise<void> {
return this.exportTemplate({
fileNameBase: 'modelo_mureg',
sheetName: 'MUREG',
headerRows: [[
'ITEM',
'LINHA ANTIGA',
'LINHA NOVA',
'ICCID',
'DATA DA MUREG',
]],
});
}
exportFaturamentoTemplate(mode: FaturamentoTemplateMode = 'ALL'): Promise<void> {
const normalizedMode = mode === 'PF' || mode === 'PJ' ? mode : 'ALL';
const suffix = normalizedMode === 'ALL' ? 'faturamento' : `faturamento_${normalizedMode.toLowerCase()}`;
const sheetName = normalizedMode === 'ALL' ? 'FATURAMENTO' : `FATURAMENTO ${normalizedMode}`;
return this.exportTemplate({
fileNameBase: `modelo_${suffix}`,
sheetName,
headerRows: [
['', '', '', 'VIVO', 'VIVO', 'LINE', 'LINE', '', '', ''],
[
'ITEM',
'CLIENTE',
'QTD DE LINHAS',
'FRANQUIA',
'VALOR CONTRATO VIVO',
'FRANQUIA LINE',
'VALOR CONTRATO LINE',
'LUCRO',
'APARELHO',
'FORMA DE PAGAMENTO',
],
],
});
}
exportDadosUsuariosTemplate(mode: DadosUsuariosTemplateMode = 'ALL'): Promise<void> {
const normalizedMode = mode === 'PF' || mode === 'PJ' ? mode : 'ALL';
if (normalizedMode === 'PF') {
return this.exportTemplate({
fileNameBase: 'modelo_dados_pf',
sheetName: 'DADOS PF',
headerRows: [[
'ITEM',
'CLIENTE',
'NOME',
'LINHA',
'CPF',
'E-MAIL',
'CELULAR',
'TELEFONE FIXO',
'RG',
'ENDEREÇO',
'DATA DE NASCIMENTO',
]],
});
}
if (normalizedMode === 'PJ') {
return this.exportTemplate({
fileNameBase: 'modelo_dados_pj',
sheetName: 'DADOS PJ',
headerRows: [[
'ITEM',
'CLIENTE',
'RAZÃO SOCIAL',
'LINHA',
'CNPJ',
'E-MAIL',
'CELULAR',
'TELEFONE FIXO',
'ENDEREÇO',
]],
});
}
return this.exportTemplate({
fileNameBase: 'modelo_dados_usuarios',
sheetName: 'DADOS USUÁRIOS',
headerRows: [[
'ITEM',
'CLIENTE',
'NOME',
'RAZÃO SOCIAL',
'LINHA',
'CPF',
'CNPJ',
'E-MAIL',
'CELULAR',
'TELEFONE FIXO',
'RG',
'ENDEREÇO',
'DATA DE NASCIMENTO',
]],
});
}
exportVigenciaTemplate(): Promise<void> {
return this.exportTemplate({
fileNameBase: 'modelo_vigencia',
sheetName: 'VIGENCIA',
headerRows: [[
'ITEM',
'CONTA',
'LINHA',
'CLIENTE',
'USUÁRIO',
'PLANO CONTRATO',
'DT. DE EFETIVAÇÃO DO SERVIÇO',
'DT. DE TÉRMINO DA FIDELIZAÇÃO',
'TOTAL',
]],
});
}
exportTrocaNumeroTemplate(): Promise<void> {
return this.exportTemplate({
fileNameBase: 'modelo_troca_numero',
sheetName: 'TROCA NUMERO',
headerRows: [[
'ITEM',
'LINHA ANTIGA',
'LINHA NOVA',
'ICCID',
'DATA DA TROCA',
'MOTIVO',
'OBSERVAÇÃO',
]],
});
}
exportChipsVirgensTemplate(): Promise<void> {
return this.exportTemplate({
fileNameBase: 'modelo_chips_virgens',
sheetName: 'CHIPS VIRGENS',
headerRows: [[
'ITEM',
'NÚMERO DO CHIP',
'OBS',
]],
});
}
exportControleRecebidosTemplate(year?: number): Promise<void> {
const suffix = typeof year === 'number' && Number.isFinite(year) ? `_${year}` : '';
return this.exportTemplate({
fileNameBase: `modelo_controle_recebidos${suffix}`,
sheetName: typeof year === 'number' && Number.isFinite(year) ? `CONTROLE ${year}` : 'CONTROLE RECEBIDOS',
headerRows: [[
'ITEM',
'NOTA FISCAL',
'CHIP',
'SERIAL',
'CONTEÚDO DA NF',
'NÚMERO DA LINHA',
'VALOR UNIT.',
'VALOR DA NF',
'DATA DA NF',
'DATA DO RECEBIMENTO',
'QTD.',
]],
});
}
exportParcelamentosTemplate(startYear: number): Promise<void> {
const baseYear = Number.isFinite(startYear) ? Math.trunc(startYear) : new Date().getFullYear();
const nextYear = baseYear + 1;
const monthHeaders = [...this.parcelamentoMonths, ...this.parcelamentoMonths];
return this.exportTemplate({
fileNameBase: `modelo_parcelamentos_${baseYear}_${nextYear}`,
sheetName: 'PARCELAMENTOS',
headerRows: [
[
'',
'',
'',
'',
'',
'',
'',
'',
...Array.from({ length: 12 }, () => String(baseYear)),
...Array.from({ length: 12 }, () => String(nextYear)),
],
[
'ANO REF',
'ITEM',
'LINHA',
'CLIENTE',
'QT PARCELAS',
'VALOR CHEIO',
'DESCONTO',
'VALOR C/ DESCONTO',
...monthHeaders,
],
],
});
}
private async exportTemplate(config: TemplateExportConfig): Promise<void> {
const headerRows = config.headerRows?.filter((row) => Array.isArray(row) && row.length > 0) ?? [];
if (!headerRows.length) {
throw new Error('Nenhum cabeçalho configurado para o modelo.');
}
const lastHeaderRow = headerRows[headerRows.length - 1];
const columns = this.buildColumns(lastHeaderRow);
await this.tableExportService.exportAsXlsx<TemplateRow>({
fileName: `${config.fileNameBase}_${this.tableExportService.buildTimestamp()}`,
sheetName: config.sheetName,
columns,
rows: [],
headerRows,
excludeItemAndIdColumns: false,
});
}
private buildColumns(headers: TemplateCell[]): TableExportColumn<TemplateRow>[] {
return headers.map((header, index) => {
const key = `col_${index + 1}`;
return {
header: String(header ?? ''),
key,
value: (row) => row[key] ?? '',
};
});
}
}

View File

@ -19,19 +19,9 @@ export interface TableExportRequest<T> {
sheetName?: string; sheetName?: string;
columns: TableExportColumn<T>[]; columns: TableExportColumn<T>[];
rows: T[]; rows: T[];
headerRows?: Array<Array<string | number | boolean | Date | null | undefined>>;
excludeItemAndIdColumns?: boolean;
templateBuffer?: ArrayBuffer | null; templateBuffer?: ArrayBuffer | null;
} }
type ExcelJsModule = typeof import('exceljs');
declare global {
interface Window {
ExcelJS?: ExcelJsModule;
}
}
type CellStyleSnapshot = { type CellStyleSnapshot = {
font?: Partial<import('exceljs').Font>; font?: Partial<import('exceljs').Font>;
fill?: import('exceljs').Fill; fill?: import('exceljs').Fill;
@ -48,54 +38,43 @@ type TemplateStyleSnapshot = {
@Injectable({ providedIn: 'root' }) @Injectable({ providedIn: 'root' })
export class TableExportService { export class TableExportService {
private readonly excelJsAssetPath = 'vendor/exceljs.min.js?v=20260312-1';
private readonly templatesApiBase = (() => { private readonly templatesApiBase = (() => {
const apiBase = buildApiBaseUrl(environment.apiUrl); const apiBase = buildApiBaseUrl(environment.apiUrl);
return `${apiBase}/templates`; return `${apiBase}/templates`;
})(); })();
private defaultTemplateBufferPromise: Promise<ArrayBuffer | null> | null = null; private defaultTemplateBufferPromise: Promise<ArrayBuffer | null> | null = null;
private cachedDefaultTemplateStyle?: TemplateStyleSnapshot; private cachedDefaultTemplateStyle?: TemplateStyleSnapshot;
private excelJsPromise: Promise<ExcelJsModule> | null = null;
constructor(private readonly http: HttpClient) {} constructor(private readonly http: HttpClient) {}
async exportAsXlsx<T>(request: TableExportRequest<T>): Promise<void> { async exportAsXlsx<T>(request: TableExportRequest<T>): Promise<void> {
const excelJsModule = await this.getExcelJs(); const ExcelJS = await import('exceljs');
const templateBuffer = request.templateBuffer ?? (await this.getDefaultTemplateBuffer()); const templateBuffer = request.templateBuffer ?? (await this.getDefaultTemplateBuffer());
const templateStyle = await this.resolveTemplateStyle(excelJsModule, templateBuffer); const templateStyle = await this.resolveTemplateStyle(ExcelJS, templateBuffer);
const workbook = new excelJsModule.Workbook(); const workbook = new ExcelJS.Workbook();
const sheet = workbook.addWorksheet(this.sanitizeSheetName(request.sheetName || 'Dados')); const sheet = workbook.addWorksheet(this.sanitizeSheetName(request.sheetName || 'Dados'));
const rawColumns = request.columns ?? []; const rawColumns = request.columns ?? [];
const excludeItemAndIdColumns = request.excludeItemAndIdColumns ?? true; const columns = rawColumns.filter((column) => !this.shouldExcludeColumnByHeader(column.header));
const columns = excludeItemAndIdColumns
? rawColumns.filter((column) => !this.shouldExcludeColumnByHeader(column.header))
: rawColumns;
const rows = request.rows ?? []; const rows = request.rows ?? [];
const explicitHeaderRows = request.headerRows ?? [];
if (!columns.length) { if (!columns.length) {
throw new Error('Nenhuma coluna exportavel apos remover ITEM/ID.'); throw new Error('Nenhuma coluna exportavel apos remover ITEM/ID.');
} }
const headerRows = explicitHeaderRows.length const headerValues = columns.map((c) => c.header ?? '');
? explicitHeaderRows.map((row) => columns.map((_, columnIndex) => row[columnIndex] ?? ''))
: [columns.map((column) => column.header ?? '')];
headerRows.forEach((headerValues) => {
sheet.addRow(headerValues); sheet.addRow(headerValues);
});
rows.forEach((row, rowIndex) => { rows.forEach((row, rowIndex) => {
const values = columns.map((column) => this.normalizeValue(column.value(row, rowIndex), column.type)); const values = columns.map((column) => this.normalizeValue(column.value(row, rowIndex), column.type));
sheet.addRow(values); sheet.addRow(values);
}); });
this.applyHeaderStyle(sheet, columns.length, headerRows.length, templateStyle); this.applyHeaderStyle(sheet, columns.length, templateStyle);
this.applyBodyStyle(sheet, columns, rows.length, headerRows.length, templateStyle); this.applyBodyStyle(sheet, columns, rows.length, templateStyle);
this.applyColumnWidths(sheet, columns, rows, headerRows, templateStyle); this.applyColumnWidths(sheet, columns, rows, templateStyle);
this.applyAutoFilter(sheet, columns.length, headerRows.length); this.applyAutoFilter(sheet, columns.length);
sheet.views = [{ state: 'frozen', ySplit: headerRows.length }]; sheet.views = [{ state: 'frozen', ySplit: 1 }];
const extensionSafeName = this.ensureXlsxExtension(request.fileName); const extensionSafeName = this.ensureXlsxExtension(request.fileName);
const buffer = await workbook.xlsx.writeBuffer(); const buffer = await workbook.xlsx.writeBuffer();
@ -114,11 +93,9 @@ export class TableExportService {
private applyHeaderStyle( private applyHeaderStyle(
sheet: import('exceljs').Worksheet, sheet: import('exceljs').Worksheet,
columnCount: number, columnCount: number,
headerRowCount: number,
templateStyle?: TemplateStyleSnapshot, templateStyle?: TemplateStyleSnapshot,
): void { ): void {
for (let rowIndex = 1; rowIndex <= headerRowCount; rowIndex += 1) { const headerRow = sheet.getRow(1);
const headerRow = sheet.getRow(rowIndex);
headerRow.height = 24; headerRow.height = 24;
for (let col = 1; col <= columnCount; col += 1) { for (let col = 1; col <= columnCount; col += 1) {
@ -134,22 +111,16 @@ export class TableExportService {
cell.border = this.cloneStyle(templateCell?.border) || this.getDefaultBorder(); cell.border = this.cloneStyle(templateCell?.border) || this.getDefaultBorder();
} }
} }
}
private applyBodyStyle<T>( private applyBodyStyle<T>(
sheet: import('exceljs').Worksheet, sheet: import('exceljs').Worksheet,
columns: TableExportColumn<T>[], columns: TableExportColumn<T>[],
rowCount: number, rowCount: number,
headerRowCount: number,
templateStyle?: TemplateStyleSnapshot, templateStyle?: TemplateStyleSnapshot,
): void { ): void {
const firstBodyRow = headerRowCount + 1; for (let rowIndex = 2; rowIndex <= rowCount + 1; rowIndex += 1) {
const lastBodyRow = headerRowCount + rowCount;
for (let rowIndex = firstBodyRow; rowIndex <= lastBodyRow; rowIndex += 1) {
const row = sheet.getRow(rowIndex); const row = sheet.getRow(rowIndex);
const bodyIndex = rowIndex - firstBodyRow; const isEven = (rowIndex - 1) % 2 === 0;
const isEven = bodyIndex % 2 === 1;
const templateRowStyle = isEven const templateRowStyle = isEven
? (templateStyle?.bodyAltStyle ?? templateStyle?.bodyStyle) ? (templateStyle?.bodyAltStyle ?? templateStyle?.bodyStyle)
: (templateStyle?.bodyStyle ?? templateStyle?.bodyAltStyle); : (templateStyle?.bodyStyle ?? templateStyle?.bodyAltStyle);
@ -183,7 +154,6 @@ export class TableExportService {
sheet: import('exceljs').Worksheet, sheet: import('exceljs').Worksheet,
columns: TableExportColumn<T>[], columns: TableExportColumn<T>[],
rows: T[], rows: T[],
headerRows: Array<Array<string | number | boolean | Date | null | undefined>>,
templateStyle?: TemplateStyleSnapshot, templateStyle?: TemplateStyleSnapshot,
): void { ): void {
columns.forEach((column, columnIndex) => { columns.forEach((column, columnIndex) => {
@ -198,11 +168,7 @@ export class TableExportService {
return; return;
} }
const explicitHeaderLength = headerRows.reduce((maxLength, headerRow) => { const headerLength = (column.header ?? '').length;
const current = String(headerRow[columnIndex] ?? '').length;
return current > maxLength ? current : maxLength;
}, 0);
const headerLength = Math.max((column.header ?? '').length, explicitHeaderLength);
let maxLength = headerLength; let maxLength = headerLength;
rows.forEach((row, rowIndex) => { rows.forEach((row, rowIndex) => {
@ -216,12 +182,11 @@ export class TableExportService {
}); });
} }
private applyAutoFilter(sheet: import('exceljs').Worksheet, columnCount: number, headerRowCount: number): void { private applyAutoFilter(sheet: import('exceljs').Worksheet, columnCount: number): void {
if (columnCount <= 0 || headerRowCount <= 0) return; if (columnCount <= 0) return;
const headerRow = headerRowCount;
sheet.autoFilter = { sheet.autoFilter = {
from: { row: headerRow, column: 1 }, from: { row: 1, column: 1 },
to: { row: headerRow, column: columnCount }, to: { row: 1, column: columnCount },
}; };
} }
@ -392,71 +357,8 @@ export class TableExportService {
return value.toString().padStart(2, '0'); return value.toString().padStart(2, '0');
} }
private async getExcelJs(): Promise<ExcelJsModule> {
if (typeof window === 'undefined' || typeof document === 'undefined') {
throw new Error('Exportacao Excel disponivel apenas no navegador.');
}
if (window.ExcelJS) {
return window.ExcelJS;
}
if (this.excelJsPromise) {
return this.excelJsPromise;
}
this.excelJsPromise = new Promise<ExcelJsModule>((resolve, reject) => {
const existingScript = document.querySelector<HTMLScriptElement>('script[data-linegestao-exceljs="true"]');
const script = existingScript ?? document.createElement('script');
const finalizeSuccess = () => {
const excelJsModule = window.ExcelJS;
if (excelJsModule) {
script.setAttribute('data-load-state', 'loaded');
resolve(excelJsModule);
return;
}
script.setAttribute('data-load-state', 'error');
reject(new Error('ExcelJS foi carregado sem expor window.ExcelJS.'));
};
const finalizeError = () => {
script.setAttribute('data-load-state', 'error');
reject(new Error('Falha ao carregar a biblioteca ExcelJS.'));
};
if (existingScript?.getAttribute('data-load-state') === 'loaded') {
finalizeSuccess();
return;
}
if (existingScript?.getAttribute('data-load-state') === 'error') {
finalizeError();
return;
}
script.addEventListener('load', finalizeSuccess, { once: true });
script.addEventListener('error', finalizeError, { once: true });
if (!existingScript) {
script.src = new URL(this.excelJsAssetPath, document.baseURI).toString();
script.async = true;
script.setAttribute('data-linegestao-exceljs', 'true');
script.setAttribute('data-load-state', 'loading');
document.head.appendChild(script);
}
});
try {
return await this.excelJsPromise;
} catch (error) {
this.excelJsPromise = null;
throw error;
}
}
private async extractTemplateStyle( private async extractTemplateStyle(
excelJsModule: ExcelJsModule, excelJsModule: typeof import('exceljs'),
templateBuffer: ArrayBuffer | null, templateBuffer: ArrayBuffer | null,
): Promise<TemplateStyleSnapshot | undefined> { ): Promise<TemplateStyleSnapshot | undefined> {
if (!templateBuffer) return undefined; if (!templateBuffer) return undefined;
@ -485,7 +387,7 @@ export class TableExportService {
} }
private async resolveTemplateStyle( private async resolveTemplateStyle(
excelJsModule: ExcelJsModule, excelJsModule: typeof import('exceljs'),
templateBuffer: ArrayBuffer | null, templateBuffer: ArrayBuffer | null,
): Promise<TemplateStyleSnapshot | undefined> { ): Promise<TemplateStyleSnapshot | undefined> {
if (templateBuffer) { if (templateBuffer) {