Compare commits
No commits in common. "31999870e4bd7ce52ca75eed54e33656e7e3a832" and "1d8f288d7b33c9b14058cf7d3ab813c50dd58399" have entirely different histories.
31999870e4
...
1d8f288d7b
10
angular.json
10
angular.json
|
|
@ -28,11 +28,6 @@
|
|||
{
|
||||
"glob": "**/*",
|
||||
"input": "public"
|
||||
},
|
||||
{
|
||||
"glob": "exceljs.min.js",
|
||||
"input": "node_modules/exceljs/dist",
|
||||
"output": "/vendor"
|
||||
}
|
||||
],
|
||||
"styles": [
|
||||
|
|
@ -104,11 +99,6 @@
|
|||
{
|
||||
"glob": "**/*",
|
||||
"input": "public"
|
||||
},
|
||||
{
|
||||
"glob": "exceljs.min.js",
|
||||
"input": "node_modules/exceljs/dist",
|
||||
"output": "/vendor"
|
||||
}
|
||||
],
|
||||
"styles": [
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
|
|
@ -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 { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
|
||||
import { Subscription } from 'rxjs';
|
||||
import { DropdownCoordinatorService } from '../../services/dropdown-coordinator.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-select',
|
||||
|
|
@ -18,9 +16,7 @@ import { DropdownCoordinatorService } from '../../services/dropdown-coordinator.
|
|||
},
|
||||
],
|
||||
})
|
||||
export class CustomSelectComponent implements ControlValueAccessor, OnDestroy {
|
||||
private static nextDropdownId = 0;
|
||||
|
||||
export class CustomSelectComponent implements ControlValueAccessor {
|
||||
@Input() options: any[] = [];
|
||||
@Input() placeholder = 'Selecione uma opção';
|
||||
@Input() labelKey = 'label';
|
||||
|
|
@ -33,22 +29,11 @@ export class CustomSelectComponent implements ControlValueAccessor, OnDestroy {
|
|||
isOpen = false;
|
||||
value: any = null;
|
||||
searchTerm = '';
|
||||
private readonly dropdownId = `custom-select-${CustomSelectComponent.nextDropdownId++}`;
|
||||
private readonly dropdownSyncSub: Subscription;
|
||||
|
||||
private onChange: (value: any) => void = () => {};
|
||||
private onTouched: () => void = () => {};
|
||||
|
||||
constructor(
|
||||
private host: ElementRef<HTMLElement>,
|
||||
private dropdownCoordinator: DropdownCoordinatorService
|
||||
) {
|
||||
this.dropdownSyncSub = this.dropdownCoordinator.activeDropdownId$.subscribe((activeId) => {
|
||||
if (this.isOpen && activeId !== this.dropdownId) {
|
||||
this.close(false);
|
||||
}
|
||||
});
|
||||
}
|
||||
constructor(private host: ElementRef<HTMLElement>) {}
|
||||
|
||||
writeValue(value: any): void {
|
||||
this.value = value;
|
||||
|
|
@ -64,7 +49,7 @@ export class CustomSelectComponent implements ControlValueAccessor, OnDestroy {
|
|||
|
||||
setDisabledState(isDisabled: boolean): void {
|
||||
this.disabled = isDisabled;
|
||||
if (this.disabled) this.close();
|
||||
if (this.disabled) this.isOpen = false;
|
||||
}
|
||||
|
||||
get displayLabel(): string {
|
||||
|
|
@ -80,21 +65,13 @@ export class CustomSelectComponent implements ControlValueAccessor, OnDestroy {
|
|||
|
||||
toggle(): void {
|
||||
if (this.disabled) return;
|
||||
if (this.isOpen) {
|
||||
this.close();
|
||||
return;
|
||||
this.isOpen = !this.isOpen;
|
||||
if (!this.isOpen) this.searchTerm = '';
|
||||
}
|
||||
|
||||
this.dropdownCoordinator.requestOpen(this.dropdownId);
|
||||
this.isOpen = true;
|
||||
}
|
||||
|
||||
close(notifyCoordinator = true): void {
|
||||
close(): void {
|
||||
this.isOpen = false;
|
||||
this.searchTerm = '';
|
||||
if (notifyCoordinator) {
|
||||
this.dropdownCoordinator.clear(this.dropdownId);
|
||||
}
|
||||
}
|
||||
|
||||
selectOption(option: any): void {
|
||||
|
|
@ -171,9 +148,4 @@ export class CustomSelectComponent implements ControlValueAccessor, OnDestroy {
|
|||
onEsc(): void {
|
||||
if (this.isOpen) this.close();
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this.close();
|
||||
this.dropdownSyncSub.unsubscribe();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -43,14 +43,6 @@
|
|||
<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>
|
||||
</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
|
||||
*ngIf="isSysAdmin && activeTab === 'chips'"
|
||||
class="btn btn-brand btn-sm"
|
||||
|
|
|
|||
|
|
@ -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 { AuthService } from '../../services/auth.service';
|
||||
import { TableExportService } from '../../services/table-export.service';
|
||||
import { ImportPageTemplateService } from '../../services/import-page-template.service';
|
||||
import { confirmDeletionWithTyping } from '../../utils/destructive-confirmation';
|
||||
import {
|
||||
buildPageNumbers,
|
||||
|
|
@ -99,7 +98,6 @@ export class ChipsControleRecebidos implements OnInit, OnDestroy {
|
|||
toastMessage = '';
|
||||
toastType: 'success' | 'danger' = 'success';
|
||||
exporting = false;
|
||||
exportingTemplate = false;
|
||||
private toastTimer: any = null;
|
||||
|
||||
chipDetailOpen = false;
|
||||
|
|
@ -139,8 +137,7 @@ export class ChipsControleRecebidos implements OnInit, OnDestroy {
|
|||
private service: ChipsControleService,
|
||||
private http: HttpClient,
|
||||
private authService: AuthService,
|
||||
private tableExportService: TableExportService,
|
||||
private importPageTemplateService: ImportPageTemplateService
|
||||
private tableExportService: TableExportService
|
||||
) {}
|
||||
|
||||
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[]> {
|
||||
if (!rows.length) return [];
|
||||
|
||||
|
|
|
|||
|
|
@ -36,10 +36,6 @@
|
|||
<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>
|
||||
</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()">
|
||||
<i class="bi bi-plus-circle me-1"></i> Novo Usuário
|
||||
</button>
|
||||
|
|
|
|||
|
|
@ -6,7 +6,6 @@ import { firstValueFrom } from 'rxjs';
|
|||
import { CustomSelectComponent } from '../../components/custom-select/custom-select';
|
||||
import { DadosUsuariosModalsComponent } from '../../components/page-modals/dados-usuarios-modals/dados-usuarios-modals';
|
||||
import { TableExportService } from '../../services/table-export.service';
|
||||
import { ImportPageTemplateService } from '../../services/import-page-template.service';
|
||||
|
||||
import {
|
||||
DadosUsuariosService,
|
||||
|
|
@ -58,7 +57,6 @@ export class DadosUsuarios implements OnInit {
|
|||
|
||||
loading = false;
|
||||
exporting = false;
|
||||
exportingTemplate = false;
|
||||
errorMsg = '';
|
||||
|
||||
// Filtros
|
||||
|
|
@ -131,8 +129,7 @@ export class DadosUsuarios implements OnInit {
|
|||
private service: DadosUsuariosService,
|
||||
private authService: AuthService,
|
||||
private linesService: LinesService,
|
||||
private tableExportService: TableExportService,
|
||||
private importPageTemplateService: ImportPageTemplateService
|
||||
private tableExportService: TableExportService
|
||||
) {}
|
||||
|
||||
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[]> {
|
||||
const pageSize = 500;
|
||||
let page = 1;
|
||||
|
|
|
|||
|
|
@ -38,10 +38,6 @@
|
|||
<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>
|
||||
</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>
|
||||
|
||||
|
|
|
|||
|
|
@ -27,7 +27,6 @@ import {
|
|||
import { AuthService } from '../../services/auth.service';
|
||||
import { LinesService } from '../../services/lines.service';
|
||||
import { TableExportService } from '../../services/table-export.service';
|
||||
import { ImportPageTemplateService } from '../../services/import-page-template.service';
|
||||
import { confirmDeletionWithTyping } from '../../utils/destructive-confirmation';
|
||||
import {
|
||||
buildPageNumbers,
|
||||
|
|
@ -68,13 +67,11 @@ export class Faturamento implements AfterViewInit, OnDestroy {
|
|||
private linesService: LinesService,
|
||||
private cdr: ChangeDetectorRef,
|
||||
private authService: AuthService,
|
||||
private tableExportService: TableExportService,
|
||||
private importPageTemplateService: ImportPageTemplateService
|
||||
private tableExportService: TableExportService
|
||||
) {}
|
||||
|
||||
loading = false;
|
||||
exporting = false;
|
||||
exportingTemplate = false;
|
||||
|
||||
// filtros
|
||||
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[] {
|
||||
const rows: BillingItem[] = [];
|
||||
this.rowsByClient.forEach((items) => rows.push(...items));
|
||||
|
|
|
|||
|
|
@ -31,11 +31,11 @@
|
|||
<small class="subtitle">Tabela de linhas e dados de telefonia</small>
|
||||
</div>
|
||||
|
||||
<div class="header-actions" data-animate>
|
||||
<div class="header-actions-stack" *ngIf="isSysAdmin; else exportOnlyTpl">
|
||||
<div class="header-actions d-flex gap-2 justify-content-end align-items-start" data-animate>
|
||||
<div class="d-flex flex-column gap-2 align-items-start" *ngIf="isSysAdmin; else exportOnlyTpl">
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-glass btn-sm header-action-btn header-action-btn-wide"
|
||||
class="btn btn-glass btn-sm"
|
||||
(click)="onImportExcel()"
|
||||
[disabled]="loading">
|
||||
<i class="bi bi-file-earmark-excel me-1"></i> Importar Dados Excel
|
||||
|
|
@ -43,7 +43,7 @@
|
|||
|
||||
<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()"
|
||||
[disabled]="loading || exporting">
|
||||
<span *ngIf="!exporting"><i class="bi bi-download me-1"></i> Exportar</span>
|
||||
|
|
@ -51,41 +51,28 @@
|
|||
</button>
|
||||
</div>
|
||||
<ng-template #exportOnlyTpl>
|
||||
<div class="header-actions-stack header-actions-stack-single">
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-glass btn-sm header-action-btn header-action-btn-export"
|
||||
class="btn btn-glass btn-sm"
|
||||
(click)="onExport()"
|
||||
[disabled]="loading || exporting">
|
||||
<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>
|
||||
</button>
|
||||
</div>
|
||||
</ng-template>
|
||||
|
||||
<div class="header-actions-secondary">
|
||||
<button
|
||||
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>
|
||||
<input #excelInput type="file" class="d-none" accept=".xlsx" (change)="onExcelSelected($event)" />
|
||||
<input #batchExcelInput type="file" class="d-none" accept=".xlsx" (change)="onBatchExcelSelected($event)" />
|
||||
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-brand btn-sm header-action-btn"
|
||||
class="btn btn-brand btn-sm align-self-start"
|
||||
*ngIf="canManageLines"
|
||||
(click)="onCadastrarLinha()"
|
||||
[disabled]="loading">
|
||||
<i class="bi bi-plus-circle me-1"></i> Novo Cliente
|
||||
</button>
|
||||
</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 class="filters-stack mt-4" data-animate>
|
||||
|
|
@ -108,21 +95,27 @@
|
|||
<i class="bi bi-box-seam me-1"></i> Estoque
|
||||
</button>
|
||||
</ng-container>
|
||||
<button type="button" class="filter-tab" [class.active]="filterStatus === 'ACTIVE'" (click)="toggleActiveFilter()" [disabled]="loading">
|
||||
<i class="bi bi-check2-circle me-1"></i> Ativos
|
||||
<button type="button" class="filter-tab" [class.active]="filterStatus === 'BLOCKED'" (click)="toggleBlockedFilter()" [disabled]="loading">
|
||||
<i class="bi bi-slash-circle me-1"></i> Bloqueadas
|
||||
</button>
|
||||
<div class="filter-blocked-select-box">
|
||||
<app-select
|
||||
class="select-glass"
|
||||
size="sm"
|
||||
[options]="blockedStatusFilterOptions"
|
||||
labelKey="label"
|
||||
valueKey="value"
|
||||
[ngModel]="blockedStatusSelectValue"
|
||||
(ngModelChange)="onBlockedStatusSelectChange($event)"
|
||||
[disabled]="loading"
|
||||
></app-select>
|
||||
</div>
|
||||
<ng-container *ngIf="filterStatus === 'BLOCKED'">
|
||||
<button
|
||||
type="button"
|
||||
class="filter-tab"
|
||||
[class.active]="blockedStatusMode === 'PERDA_ROUBO'"
|
||||
(click)="setBlockedStatusMode('PERDA_ROUBO')"
|
||||
[disabled]="loading">
|
||||
Perda/Roubo
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="filter-tab"
|
||||
[class.active]="blockedStatusMode === 'BLOQUEIO_120'"
|
||||
(click)="setBlockedStatusMode('BLOQUEIO_120')"
|
||||
[disabled]="loading">
|
||||
120 dias
|
||||
</button>
|
||||
</ng-container>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
@ -351,7 +344,16 @@
|
|||
</button>
|
||||
</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">
|
||||
<span class="batch-status-count">
|
||||
Selecionadas: <strong>{{ batchStatusSelectionCount }}</strong>
|
||||
|
|
@ -371,17 +373,6 @@
|
|||
<i class="bi bi-unlock me-1"></i> Desbloquear em lote
|
||||
</button>
|
||||
</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>
|
||||
|
||||
|
|
|
|||
|
|
@ -111,107 +111,12 @@
|
|||
/* 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; }
|
||||
.header-row-top {
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
.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; } } }
|
||||
.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; }
|
||||
.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; }
|
||||
.header-actions {
|
||||
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;
|
||||
}
|
||||
}
|
||||
.header-actions { justify-self: end; }
|
||||
|
||||
/* 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; } }
|
||||
|
|
@ -238,45 +143,13 @@
|
|||
|
||||
.filters-row-top {
|
||||
justify-content: center;
|
||||
z-index: 60;
|
||||
}
|
||||
|
||||
.filters-row-bottom {
|
||||
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; }
|
||||
.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 { 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; } } }
|
||||
.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 {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
flex-wrap: wrap;
|
||||
margin-left: auto;
|
||||
|
||||
@media (max-width: 900px) {
|
||||
margin-left: 0;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,11 +1,9 @@
|
|||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http';
|
||||
import { provideHttpClientTesting } from '@angular/common/http/testing';
|
||||
import { HttpParams } from '@angular/common/http';
|
||||
import { provideRouter } from '@angular/router';
|
||||
|
||||
import { Geral } from './geral';
|
||||
import { DropdownCoordinatorService } from '../../services/dropdown-coordinator.service';
|
||||
|
||||
describe('Geral', () => {
|
||||
let component: Geral;
|
||||
|
|
@ -107,85 +105,4 @@ describe('Geral', () => {
|
|||
expect(filtered.length).toBe(1);
|
||||
expect(filtered[0].conta).toBe('460161507');
|
||||
});
|
||||
|
||||
it('should classify reserve-assigned line as RESERVA during smart search resolution', () => {
|
||||
const skilFilter = (component as any).resolveSkilFilterFromLine({
|
||||
cliente: 'AVANCO DISTRIBUIDORA',
|
||||
usuario: 'RESERVA',
|
||||
skil: 'PESSOA JURIDICA',
|
||||
});
|
||||
|
||||
expect(skilFilter).toBe('RESERVA');
|
||||
});
|
||||
|
||||
it('should classify stock line as ESTOQUE during smart search resolution', () => {
|
||||
const skilFilter = (component as any).resolveSkilFilterFromLine({
|
||||
cliente: 'ESTOQUE',
|
||||
usuario: 'RESERVA',
|
||||
skil: 'RESERVA',
|
||||
});
|
||||
|
||||
expect(skilFilter).toBe('ESTOQUE');
|
||||
});
|
||||
|
||||
it('should filter only active lines when ACTIVE status filter is selected', () => {
|
||||
component.filterStatus = 'ACTIVE';
|
||||
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: 'ATIVO' },
|
||||
{ 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('ATIVO');
|
||||
});
|
||||
|
||||
it('should classify active line as ACTIVE 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: 'ATIVO',
|
||||
skil: 'PESSOA JURIDICA',
|
||||
}, true);
|
||||
|
||||
expect(target?.statusFilter).toBe('ACTIVE');
|
||||
});
|
||||
|
||||
it('should request assigned reserve lines in ALL filter only', () => {
|
||||
component.filterSkil = 'ALL';
|
||||
let params = (component as any).applyBaseFilters(new HttpParams());
|
||||
|
||||
expect(params.get('includeAssignedReservaInAll')).toBe('true');
|
||||
|
||||
component.filterSkil = 'RESERVA';
|
||||
params = (component as any).applyBaseFilters(new HttpParams());
|
||||
|
||||
expect(params.get('includeAssignedReservaInAll')).toBeNull();
|
||||
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();
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -24,7 +24,6 @@ import { PlanAutoFillService } from '../../services/plan-autofill.service';
|
|||
import { AuthService } from '../../services/auth.service';
|
||||
import { TenantSyncService } from '../../services/tenant-sync.service';
|
||||
import { TableExportService } from '../../services/table-export.service';
|
||||
import { ImportPageTemplateService } from '../../services/import-page-template.service';
|
||||
import { SolicitacoesLinhasService } from '../../services/solicitacoes-linhas.service';
|
||||
import {
|
||||
MveAuditService,
|
||||
|
|
@ -32,7 +31,6 @@ import {
|
|||
type MveAuditIssue,
|
||||
type MveAuditRun,
|
||||
} from '../../services/mve-audit.service';
|
||||
import { DropdownCoordinatorService } from '../../services/dropdown-coordinator.service';
|
||||
import { firstValueFrom, Subscription, filter } from 'rxjs';
|
||||
import { environment } from '../../../environments/environment';
|
||||
import { confirmDeletionWithTyping } from '../../utils/destructive-confirmation';
|
||||
|
|
@ -69,8 +67,7 @@ type CreateEntryMode = 'SINGLE' | 'BATCH';
|
|||
type AdditionalMode = 'ALL' | 'WITH' | 'WITHOUT';
|
||||
type OperadoraFilterMode = 'ALL' | 'VIVO' | 'CLARO' | 'TIM';
|
||||
type AdditionalServiceKey = 'gvd' | 'skeelo' | 'news' | 'travel' | 'sync' | 'dispositivo';
|
||||
type BlockedStatusMode = 'ALL' | 'PERDA_ROUBO' | 'BLOQUEIO_120' | 'PRE_ATIVACAO';
|
||||
type BlockedStatusFilterValue = '' | BlockedStatusMode;
|
||||
type BlockedStatusMode = 'ALL' | 'PERDA_ROUBO' | 'BLOQUEIO_120';
|
||||
type SkilFilterMode = 'ALL' | 'PF' | 'PJ' | 'RESERVA' | 'ESTOQUE';
|
||||
|
||||
interface LineRow {
|
||||
|
|
@ -124,7 +121,7 @@ interface ApiLineList {
|
|||
interface SmartSearchTargetResolution {
|
||||
client: string;
|
||||
skilFilter: SkilFilterMode;
|
||||
statusFilter: 'ALL' | 'ACTIVE' | 'BLOCKED';
|
||||
statusFilter: 'ALL' | 'BLOCKED';
|
||||
blockedStatusMode: BlockedStatusMode;
|
||||
requiresFilterAdjustment: boolean;
|
||||
}
|
||||
|
|
@ -364,14 +361,9 @@ interface MveApplySelectionSummary {
|
|||
styleUrls: ['./geral.scss']
|
||||
})
|
||||
export class Geral implements OnInit, AfterViewInit, OnDestroy {
|
||||
private static nextFilterDropdownScopeId = 0;
|
||||
|
||||
readonly vm = this;
|
||||
readonly batchMassColumnGuide = BATCH_MASS_COLUMN_GUIDE;
|
||||
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('excelInput') excelInput!: ElementRef<HTMLInputElement>;
|
||||
|
|
@ -393,16 +385,13 @@ export class Geral implements OnInit, AfterViewInit, OnDestroy {
|
|||
private tenantSyncService: TenantSyncService,
|
||||
private solicitacoesLinhasService: SolicitacoesLinhasService,
|
||||
private tableExportService: TableExportService,
|
||||
private importPageTemplateService: ImportPageTemplateService,
|
||||
private mveAuditService: MveAuditService,
|
||||
private dropdownCoordinator: DropdownCoordinatorService
|
||||
private mveAuditService: MveAuditService
|
||||
) {}
|
||||
|
||||
private readonly apiBase = buildApiEndpoint(environment.apiUrl, 'lines');
|
||||
private readonly templatesApiBase = buildApiEndpoint(environment.apiUrl, 'templates');
|
||||
loading = false;
|
||||
exporting = false;
|
||||
exportingTemplate = false;
|
||||
isSysAdmin = false;
|
||||
isGestor = false;
|
||||
isFinanceiro = false;
|
||||
|
|
@ -416,19 +405,12 @@ export class Geral implements OnInit, AfterViewInit, OnDestroy {
|
|||
|
||||
searchTerm = '';
|
||||
filterSkil: SkilFilterMode = 'ALL';
|
||||
filterStatus: 'ALL' | 'ACTIVE' | 'BLOCKED' = 'ALL';
|
||||
filterStatus: 'ALL' | 'BLOCKED' = 'ALL';
|
||||
blockedStatusMode: BlockedStatusMode = 'ALL';
|
||||
additionalMode: AdditionalMode = 'ALL';
|
||||
selectedAdditionalServices: AdditionalServiceKey[] = [];
|
||||
filterOperadora: OperadoraFilterMode = 'ALL';
|
||||
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 }> = [
|
||||
{ key: 'gvd', label: 'Gestão Voz e Dados' },
|
||||
{ key: 'skeelo', label: 'Skeelo' },
|
||||
|
|
@ -527,7 +509,6 @@ export class Geral implements OnInit, AfterViewInit, OnDestroy {
|
|||
private editingId: string | null = null;
|
||||
private searchTimer: any = null;
|
||||
private navigationSub?: Subscription;
|
||||
private dropdownSyncSub?: Subscription;
|
||||
private keepPageOnNextGroupsLoad = false;
|
||||
|
||||
private searchResolvedClient: string | null = null;
|
||||
|
|
@ -544,7 +525,7 @@ export class Geral implements OnInit, AfterViewInit, OnDestroy {
|
|||
kpiAtivas = 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'];
|
||||
|
||||
planOptions = [
|
||||
|
|
@ -727,10 +708,6 @@ export class Geral implements OnInit, AfterViewInit, OnDestroy {
|
|||
return (this.batchExcelPreview?.rows ?? []).slice(0, 8);
|
||||
}
|
||||
|
||||
get blockedStatusSelectValue(): BlockedStatusFilterValue {
|
||||
return this.filterStatus === 'BLOCKED' ? this.blockedStatusMode : '';
|
||||
}
|
||||
|
||||
getBatchExcelRowErrorsTitle(row: BatchExcelPreviewRowDto | null | undefined): string {
|
||||
const errors = row?.errors ?? [];
|
||||
return errors
|
||||
|
|
@ -854,7 +831,7 @@ export class Geral implements OnInit, AfterViewInit, OnDestroy {
|
|||
}
|
||||
|
||||
get hasClientSideFiltersApplied(): boolean {
|
||||
return this.hasAdditionalFiltersApplied || this.filterStatus !== 'ALL' || this.hasOperadoraEmpresaFiltersApplied;
|
||||
return this.hasAdditionalFiltersApplied || this.filterStatus === 'BLOCKED' || this.hasOperadoraEmpresaFiltersApplied;
|
||||
}
|
||||
|
||||
get additionalModeLabel(): string {
|
||||
|
|
@ -991,12 +968,12 @@ export class Geral implements OnInit, AfterViewInit, OnDestroy {
|
|||
let changed = false;
|
||||
|
||||
if (this.showClientMenu && !insideClient) {
|
||||
this.closeClientDropdown();
|
||||
this.showClientMenu = false;
|
||||
changed = true;
|
||||
}
|
||||
|
||||
if (this.showAdditionalMenu && !insideAdditional) {
|
||||
this.closeAdditionalDropdown();
|
||||
this.showAdditionalMenu = false;
|
||||
changed = true;
|
||||
}
|
||||
|
||||
|
|
@ -1022,12 +999,12 @@ export class Geral implements OnInit, AfterViewInit, OnDestroy {
|
|||
let changed = false;
|
||||
|
||||
if (this.showClientMenu) {
|
||||
this.closeClientDropdown();
|
||||
this.showClientMenu = false;
|
||||
changed = true;
|
||||
}
|
||||
|
||||
if (this.showAdditionalMenu) {
|
||||
this.closeAdditionalDropdown();
|
||||
this.showAdditionalMenu = false;
|
||||
changed = true;
|
||||
}
|
||||
|
||||
|
|
@ -1041,7 +1018,6 @@ export class Geral implements OnInit, AfterViewInit, OnDestroy {
|
|||
ngOnDestroy(): void {
|
||||
if (this.searchTimer) clearTimeout(this.searchTimer);
|
||||
this.navigationSub?.unsubscribe();
|
||||
this.dropdownSyncSub?.unsubscribe();
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
|
|
@ -1059,16 +1035,6 @@ export class Geral implements OnInit, AfterViewInit, OnDestroy {
|
|||
this.selectedAdditionalServices = [];
|
||||
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() {
|
||||
|
|
@ -1183,19 +1149,10 @@ export class Geral implements OnInit, AfterViewInit, OnDestroy {
|
|||
return null;
|
||||
}
|
||||
|
||||
private parseQueryStatusFilter(value: unknown): 'ALL' | 'ACTIVE' | 'BLOCKED' | null {
|
||||
private parseQueryStatusFilter(value: unknown): 'ALL' | 'BLOCKED' | null {
|
||||
const token = this.normalizeFilterToken(value);
|
||||
if (!token) return null;
|
||||
if (token === 'ALL' || token === 'TODOS') return 'ALL';
|
||||
if (
|
||||
token === 'ACTIVE' ||
|
||||
token === 'ATIVAS' ||
|
||||
token === 'ATIVOS' ||
|
||||
token === 'ATIVA' ||
|
||||
token === 'ATIVO'
|
||||
) {
|
||||
return 'ACTIVE';
|
||||
}
|
||||
if (
|
||||
token === 'BLOCKED' ||
|
||||
token === 'BLOQUEADAS' ||
|
||||
|
|
@ -1229,13 +1186,6 @@ export class Geral implements OnInit, AfterViewInit, OnDestroy {
|
|||
) {
|
||||
return 'BLOQUEIO_120';
|
||||
}
|
||||
if (
|
||||
token === 'PREATIVACAO' ||
|
||||
token === 'PREATIV' ||
|
||||
token === 'BLOQUEIOPREATIVACAO'
|
||||
) {
|
||||
return 'PRE_ATIVACAO';
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
|
|
@ -1446,7 +1396,7 @@ export class Geral implements OnInit, AfterViewInit, OnDestroy {
|
|||
refreshData(opts?: { keepCurrentPage?: boolean }) {
|
||||
const keepCurrentPage = !!opts?.keepCurrentPage;
|
||||
this.keepPageOnNextGroupsLoad = keepCurrentPage;
|
||||
if (!keepCurrentPage && (this.isReserveContextFilter() || this.filterStatus !== 'ALL')) {
|
||||
if (!keepCurrentPage && (this.isReserveContextFilter() || this.filterStatus === 'BLOCKED')) {
|
||||
this.page = 1;
|
||||
}
|
||||
this.searchResolvedClient = null;
|
||||
|
|
@ -1472,18 +1422,9 @@ export class Geral implements OnInit, AfterViewInit, OnDestroy {
|
|||
return String(value ?? '').replace(/\D/g, '');
|
||||
}
|
||||
|
||||
private isReservaValue(value: unknown): boolean {
|
||||
return this.normalizeFilterToken(value) === 'RESERVA';
|
||||
}
|
||||
|
||||
private resolveSkilFilterFromLine(line: Pick<ApiLineList, 'skil' | 'cliente' | 'usuario'> | null | undefined): SkilFilterMode {
|
||||
if (!line) return 'ALL';
|
||||
if (this.isStockClientName(line.cliente)) return 'ESTOQUE';
|
||||
if (this.isReservaValue(line.cliente) || this.isReservaValue(line.usuario) || this.isReservaValue(line.skil)) {
|
||||
return 'RESERVA';
|
||||
}
|
||||
|
||||
const parsed = this.parseQuerySkilFilter(line.skil);
|
||||
private resolveSkilFilterFromLine(skil: unknown, client: unknown): SkilFilterMode {
|
||||
if (this.isStockClientName(client)) return 'ESTOQUE';
|
||||
const parsed = this.parseQuerySkilFilter(skil);
|
||||
return parsed ?? 'ALL';
|
||||
}
|
||||
|
||||
|
|
@ -1541,8 +1482,6 @@ export class Geral implements OnInit, AfterViewInit, OnDestroy {
|
|||
if (!options?.ignoreCurrentFilters) {
|
||||
params = this.applyBaseFilters(params);
|
||||
this.selectedClients.forEach((c) => (params = params.append('client', c)));
|
||||
} else if (!options?.skilFilter) {
|
||||
params = params.set('includeAssignedReservaInAll', 'true');
|
||||
} else if (options?.skilFilter === 'PF') {
|
||||
params = params.set('skil', 'PESSOA FÍSICA');
|
||||
} else if (options?.skilFilter === 'PJ') {
|
||||
|
|
@ -1575,19 +1514,14 @@ export class Geral implements OnInit, AfterViewInit, OnDestroy {
|
|||
): SmartSearchTargetResolution | null {
|
||||
if (!line) return null;
|
||||
|
||||
const skilFilter = this.resolveSkilFilterFromLine(line);
|
||||
const skilFilter = this.resolveSkilFilterFromLine(line?.skil, line?.cliente);
|
||||
const blockedStatusMode = this.resolveBlockedStatusMode(line?.status ?? '') ?? 'ALL';
|
||||
const statusFilter = blockedStatusMode !== 'ALL'
|
||||
? 'BLOCKED'
|
||||
: this.isActiveStatus(line?.status ?? '')
|
||||
? 'ACTIVE'
|
||||
: 'ALL';
|
||||
const client = ((line?.cliente ?? '').toString().trim()) || this.getClientFallbackLabel('SEM CLIENTE', skilFilter);
|
||||
|
||||
return {
|
||||
client,
|
||||
skilFilter,
|
||||
statusFilter,
|
||||
statusFilter: blockedStatusMode === 'ALL' ? 'ALL' : 'BLOCKED',
|
||||
blockedStatusMode,
|
||||
requiresFilterAdjustment
|
||||
};
|
||||
|
|
@ -1846,58 +1780,13 @@ export class Geral implements OnInit, AfterViewInit, OnDestroy {
|
|||
this.refreshData();
|
||||
}
|
||||
|
||||
toggleActiveFilter() {
|
||||
if (this.filterStatus === 'ACTIVE') {
|
||||
this.filterStatus = 'ALL';
|
||||
} else {
|
||||
this.filterStatus = 'ACTIVE';
|
||||
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();
|
||||
}
|
||||
|
||||
setBlockedStatusFilter(mode: BlockedStatusFilterValue) {
|
||||
const normalizedMode = mode ?? '';
|
||||
|
||||
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) {
|
||||
toggleBlockedFilter() {
|
||||
if (this.filterStatus === 'BLOCKED') {
|
||||
this.filterStatus = 'ALL';
|
||||
this.blockedStatusMode = 'ALL';
|
||||
} else {
|
||||
this.filterStatus = 'BLOCKED';
|
||||
this.blockedStatusMode = normalizedMode;
|
||||
}
|
||||
|
||||
this.expandedGroup = null;
|
||||
this.groupLines = [];
|
||||
this.searchResolvedClient = null;
|
||||
|
|
@ -1912,8 +1801,24 @@ export class Geral implements OnInit, AfterViewInit, OnDestroy {
|
|||
this.refreshData();
|
||||
}
|
||||
|
||||
onBlockedStatusSelectChange(value: BlockedStatusFilterValue) {
|
||||
this.setBlockedStatusFilter(value);
|
||||
setBlockedStatusMode(mode: Exclude<BlockedStatusMode, 'ALL'>) {
|
||||
if (this.filterStatus !== 'BLOCKED') {
|
||||
this.filterStatus = 'BLOCKED';
|
||||
}
|
||||
|
||||
this.blockedStatusMode = this.blockedStatusMode === mode ? 'ALL' : mode;
|
||||
this.expandedGroup = null;
|
||||
this.groupLines = [];
|
||||
this.searchResolvedClient = null;
|
||||
this.selectedClients = [];
|
||||
this.clientSearchTerm = '';
|
||||
this.page = 1;
|
||||
|
||||
if (!this.isClientRestricted) {
|
||||
this.loadClients();
|
||||
}
|
||||
|
||||
this.refreshData();
|
||||
}
|
||||
|
||||
setAdditionalMode(mode: AdditionalMode) {
|
||||
|
|
@ -1997,14 +1902,11 @@ export class Geral implements OnInit, AfterViewInit, OnDestroy {
|
|||
next = next.set('skil', 'RESERVA');
|
||||
const reservaMode = this.getReservaModeForApi();
|
||||
if (reservaMode) next = next.set('reservaMode', reservaMode);
|
||||
} else {
|
||||
next = next.set('includeAssignedReservaInAll', 'true');
|
||||
}
|
||||
if (this.filterStatus === 'BLOCKED') {
|
||||
next = next.set('statusMode', 'blocked');
|
||||
if (this.blockedStatusMode === 'PERDA_ROUBO') next = next.set('statusSubtype', 'perda_roubo');
|
||||
else if (this.blockedStatusMode === 'BLOQUEIO_120') next = next.set('statusSubtype', '120_dias');
|
||||
else if (this.blockedStatusMode === 'PRE_ATIVACAO') next = next.set('statusSubtype', 'pre_ativacao');
|
||||
}
|
||||
|
||||
if (this.additionalMode === 'WITH') next = next.set('additionalMode', 'with');
|
||||
|
|
@ -2051,28 +1953,21 @@ export class Geral implements OnInit, AfterViewInit, OnDestroy {
|
|||
normalized.includes('BLOQUE') ||
|
||||
normalized.includes('PERDA') ||
|
||||
normalized.includes('ROUBO') ||
|
||||
normalized.includes('FURTO') ||
|
||||
normalized.includes('PREATIV');
|
||||
normalized.includes('FURTO');
|
||||
if (!hasBlockedToken) return null;
|
||||
|
||||
if (normalized.includes('120')) return 'BLOQUEIO_120';
|
||||
if (normalized.includes('PREATIV')) return 'PRE_ATIVACAO';
|
||||
if (normalized.includes('PERDA') || normalized.includes('ROUBO') || normalized.includes('FURTO')) {
|
||||
return 'PERDA_ROUBO';
|
||||
}
|
||||
|
||||
return 'PRE_ATIVACAO';
|
||||
return 'PERDA_ROUBO';
|
||||
}
|
||||
|
||||
private isBlockedStatus(status: unknown): boolean {
|
||||
return this.resolveBlockedStatusMode(status) !== null;
|
||||
}
|
||||
|
||||
private isActiveStatus(status: unknown): boolean {
|
||||
if (this.isBlockedStatus(status)) return false;
|
||||
return this.normalizeFilterToken(status).includes('ATIVO');
|
||||
}
|
||||
|
||||
private matchesBlockedStatusMode(status: unknown): boolean {
|
||||
const mode = this.resolveBlockedStatusMode(status);
|
||||
if (!mode) return false;
|
||||
|
|
@ -2084,9 +1979,6 @@ export class Geral implements OnInit, AfterViewInit, OnDestroy {
|
|||
if (this.filterStatus === 'BLOCKED' && !this.matchesBlockedStatusMode(line?.status ?? '')) {
|
||||
return false;
|
||||
}
|
||||
if (this.filterStatus === 'ACTIVE' && !this.isActiveStatus(line?.status ?? '')) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!this.matchesOperadoraContaEmpresaFilters(line)) {
|
||||
return false;
|
||||
|
|
@ -2300,7 +2192,7 @@ export class Geral implements OnInit, AfterViewInit, OnDestroy {
|
|||
const keepCurrentPage = this.keepPageOnNextGroupsLoad;
|
||||
this.keepPageOnNextGroupsLoad = false;
|
||||
|
||||
if (!keepCurrentPage && (this.isReserveContextFilter() || this.filterStatus !== 'ALL') && !hasSelection && !hasResolved) {
|
||||
if (!keepCurrentPage && (this.isReserveContextFilter() || this.filterStatus === 'BLOCKED') && !hasSelection && !hasResolved) {
|
||||
this.page = 1;
|
||||
}
|
||||
|
||||
|
|
@ -2588,43 +2480,27 @@ export class Geral implements OnInit, AfterViewInit, OnDestroy {
|
|||
|
||||
toggleClientMenu() {
|
||||
if (this.isClientRestricted) return;
|
||||
if (this.showClientMenu) {
|
||||
this.closeClientDropdown();
|
||||
return;
|
||||
}
|
||||
|
||||
this.dropdownCoordinator.requestOpen(this.clientDropdownId);
|
||||
this.showClientMenu = true;
|
||||
if (!this.showClientMenu) this.showAdditionalMenu = false;
|
||||
this.showClientMenu = !this.showClientMenu;
|
||||
}
|
||||
|
||||
toggleAdditionalMenu() {
|
||||
if (this.isClientRestricted) return;
|
||||
if (this.showAdditionalMenu) {
|
||||
this.closeAdditionalDropdown();
|
||||
return;
|
||||
if (!this.showAdditionalMenu) this.showClientMenu = false;
|
||||
this.showAdditionalMenu = !this.showAdditionalMenu;
|
||||
}
|
||||
|
||||
this.dropdownCoordinator.requestOpen(this.additionalDropdownId);
|
||||
this.showAdditionalMenu = true;
|
||||
}
|
||||
|
||||
closeClientDropdown(notifyCoordinator = true) {
|
||||
closeClientDropdown() {
|
||||
this.showClientMenu = false;
|
||||
if (notifyCoordinator) {
|
||||
this.dropdownCoordinator.clear(this.clientDropdownId);
|
||||
}
|
||||
}
|
||||
|
||||
closeAdditionalDropdown(notifyCoordinator = true) {
|
||||
closeAdditionalDropdown() {
|
||||
this.showAdditionalMenu = false;
|
||||
if (notifyCoordinator) {
|
||||
this.dropdownCoordinator.clear(this.additionalDropdownId);
|
||||
}
|
||||
}
|
||||
|
||||
closeFilterDropdowns() {
|
||||
this.closeClientDropdown();
|
||||
this.closeAdditionalDropdown();
|
||||
this.showClientMenu = false;
|
||||
this.showAdditionalMenu = false;
|
||||
}
|
||||
|
||||
selectClient(client: string | null) {
|
||||
|
|
@ -2827,20 +2703,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[]> {
|
||||
let lines = await this.fetchLinesForGrouping();
|
||||
|
||||
|
|
@ -2981,12 +2843,9 @@ export class Geral implements OnInit, AfterViewInit, OnDestroy {
|
|||
else if (this.filterSkil === 'ESTOQUE') parts.push('estoque');
|
||||
else parts.push('todas');
|
||||
|
||||
if (this.filterStatus === 'ACTIVE') {
|
||||
parts.push('ativas');
|
||||
} else if (this.filterStatus === 'BLOCKED') {
|
||||
if (this.filterStatus === 'BLOCKED') {
|
||||
if (this.blockedStatusMode === 'PERDA_ROUBO') parts.push('bloq-perda-roubo');
|
||||
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');
|
||||
}
|
||||
|
||||
|
|
@ -5307,6 +5166,12 @@ export class Geral implements OnInit, AfterViewInit, OnDestroy {
|
|||
return v || '-';
|
||||
}
|
||||
|
||||
private isActiveStatus(status: string | null | undefined): boolean {
|
||||
const normalized = (status ?? '').toString().trim().toLowerCase();
|
||||
if (!normalized) return false;
|
||||
return normalized.includes('ativo');
|
||||
}
|
||||
|
||||
private toEditModel(d: ApiLineDetail): any {
|
||||
return {
|
||||
...d,
|
||||
|
|
|
|||
|
|
@ -35,10 +35,6 @@
|
|||
<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>
|
||||
</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">
|
||||
<i class="bi bi-plus-circle me-1"></i> Nova Mureg
|
||||
</button>
|
||||
|
|
|
|||
|
|
@ -15,7 +15,6 @@ import { AuthService } from '../../services/auth.service';
|
|||
import { LinesService } from '../../services/lines.service';
|
||||
import { CustomSelectComponent } from '../../components/custom-select/custom-select';
|
||||
import { TableExportService } from '../../services/table-export.service';
|
||||
import { ImportPageTemplateService } from '../../services/import-page-template.service';
|
||||
import { environment } from '../../../environments/environment';
|
||||
import { confirmDeletionWithTyping } from '../../utils/destructive-confirmation';
|
||||
import { MuregModalsComponent } from '../../components/page-modals/mureg-modals/mureg-modals';
|
||||
|
|
@ -110,7 +109,6 @@ export class Mureg implements AfterViewInit {
|
|||
toastMessage = '';
|
||||
loading = false;
|
||||
exporting = false;
|
||||
exportingTemplate = false;
|
||||
|
||||
@ViewChild('successToast', { static: false }) successToast!: ElementRef;
|
||||
|
||||
|
|
@ -120,8 +118,7 @@ export class Mureg implements AfterViewInit {
|
|||
private cdr: ChangeDetectorRef,
|
||||
private authService: AuthService,
|
||||
private linesService: LinesService,
|
||||
private tableExportService: TableExportService,
|
||||
private importPageTemplateService: ImportPageTemplateService
|
||||
private tableExportService: TableExportService
|
||||
) {}
|
||||
|
||||
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[]> {
|
||||
const pageSize = 2000;
|
||||
let page = 1;
|
||||
|
|
|
|||
|
|
@ -463,11 +463,11 @@
|
|||
}
|
||||
|
||||
thead th:nth-child(4) {
|
||||
width: 20%;
|
||||
width: 24%;
|
||||
}
|
||||
|
||||
thead th:nth-child(5) {
|
||||
width: 16%;
|
||||
width: 12%;
|
||||
}
|
||||
|
||||
thead th {
|
||||
|
|
@ -508,12 +508,6 @@
|
|||
text-align: center;
|
||||
}
|
||||
|
||||
.cell-situation,
|
||||
.cell-action {
|
||||
padding-left: 12px;
|
||||
padding-right: 12px;
|
||||
}
|
||||
|
||||
.cell-compare {
|
||||
text-align: left;
|
||||
}
|
||||
|
|
@ -546,16 +540,12 @@
|
|||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-width: min(100%, 190px);
|
||||
max-width: 100%;
|
||||
padding: 10px 16px;
|
||||
padding: 6px 10px;
|
||||
border-radius: 999px;
|
||||
font-size: 13px;
|
||||
font-size: 11px;
|
||||
font-weight: 900;
|
||||
line-height: 1.15;
|
||||
letter-spacing: 0.02em;
|
||||
line-height: 1;
|
||||
border: 1px solid transparent;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.issue-kind-badge {
|
||||
|
|
@ -719,17 +709,16 @@
|
|||
.situation-card {
|
||||
display: grid;
|
||||
gap: 12px;
|
||||
width: min(100%, 240px);
|
||||
min-height: 112px;
|
||||
margin-inline: auto;
|
||||
padding: 10px 8px;
|
||||
padding: 14px;
|
||||
border-radius: 18px;
|
||||
background: transparent;
|
||||
box-shadow: none;
|
||||
border: 0;
|
||||
align-content: center;
|
||||
background: linear-gradient(180deg, rgba(255, 255, 255, 0.97), rgba(248, 246, 250, 0.93));
|
||||
box-shadow: 0 14px 28px rgba(24, 17, 33, 0.05);
|
||||
justify-items: center;
|
||||
text-align: center;
|
||||
|
||||
&.is-applied {
|
||||
background: linear-gradient(180deg, rgba(25, 135, 84, 0.06), rgba(255, 255, 255, 0.98));
|
||||
}
|
||||
}
|
||||
|
||||
.situation-top {
|
||||
|
|
@ -742,38 +731,23 @@
|
|||
|
||||
.action-card {
|
||||
display: grid;
|
||||
width: min(100%, 188px);
|
||||
min-height: 112px;
|
||||
margin-inline: auto;
|
||||
padding: 10px 8px;
|
||||
gap: 12px;
|
||||
align-content: center;
|
||||
gap: 10px;
|
||||
justify-items: center;
|
||||
text-align: center;
|
||||
border-radius: 18px;
|
||||
border: 0;
|
||||
background: transparent;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.sync-badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
max-width: 100%;
|
||||
padding: 10px 14px;
|
||||
padding: 7px 12px;
|
||||
border-radius: 999px;
|
||||
font-size: 12px;
|
||||
font-weight: 900;
|
||||
line-height: 1.15;
|
||||
text-align: center;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
|
||||
&.ready {
|
||||
background: rgba(255, 178, 0, 0.16);
|
||||
color: #8c6200;
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
&.applied {
|
||||
|
|
@ -789,8 +763,6 @@
|
|||
|
||||
.page-footer {
|
||||
margin-top: 16px;
|
||||
padding: 14px 24px 0;
|
||||
border-top: 1px solid rgba(17, 18, 20, 0.06);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
|
|
@ -798,35 +770,6 @@
|
|||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.pagination-modern .page-link {
|
||||
color: #030faa;
|
||||
font-weight: 900;
|
||||
border-radius: 10px;
|
||||
border: 1px solid rgba(17, 18, 20, 0.1);
|
||||
background: rgba(255, 255, 255, 0.6);
|
||||
margin: 0 2px;
|
||||
transition: transform 160ms ease, border-color 160ms ease, color 160ms ease, background-color 160ms ease;
|
||||
|
||||
&:hover {
|
||||
transform: translateY(-1px);
|
||||
border-color: var(--brand);
|
||||
color: var(--brand);
|
||||
background: rgba(255, 255, 255, 0.92);
|
||||
}
|
||||
}
|
||||
|
||||
.pagination-modern .page-item.active .page-link {
|
||||
background-color: #030faa;
|
||||
border-color: #030faa;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.pagination-modern .page-item.disabled .page-link {
|
||||
color: rgba(24, 17, 33, 0.42);
|
||||
background: rgba(255, 255, 255, 0.38);
|
||||
border-color: rgba(17, 18, 20, 0.08);
|
||||
}
|
||||
|
||||
.status-empty,
|
||||
.empty-state {
|
||||
padding: 42px 20px;
|
||||
|
|
@ -965,11 +908,6 @@
|
|||
padding: 7px 12px;
|
||||
}
|
||||
|
||||
.situation-card,
|
||||
.action-card {
|
||||
width: min(100%, 220px);
|
||||
}
|
||||
|
||||
.page-footer {
|
||||
justify-content: center;
|
||||
text-align: center;
|
||||
|
|
@ -1004,11 +942,6 @@
|
|||
font-size: 11px;
|
||||
}
|
||||
|
||||
.issue-kind-badge {
|
||||
font-size: 11px;
|
||||
padding: 8px 12px;
|
||||
}
|
||||
|
||||
.page-footer .pagination {
|
||||
justify-content: center;
|
||||
flex-wrap: wrap;
|
||||
|
|
|
|||
|
|
@ -29,10 +29,6 @@
|
|||
<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>
|
||||
</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()">
|
||||
<i class="bi bi-plus-circle"></i> Novo Parcelamento
|
||||
</button>
|
||||
|
|
|
|||
|
|
@ -6,7 +6,6 @@ import { finalize, Subscription, firstValueFrom, timeout } from 'rxjs';
|
|||
import { ParcelamentosModalsComponent } from '../../components/page-modals/parcelamento-modals/parcelamentos-modals';
|
||||
import { AuthService } from '../../services/auth.service';
|
||||
import { TableExportService } from '../../services/table-export.service';
|
||||
import { ImportPageTemplateService } from '../../services/import-page-template.service';
|
||||
import {
|
||||
ParcelamentosService,
|
||||
ParcelamentoListItem,
|
||||
|
|
@ -69,7 +68,6 @@ export class Parcelamentos implements OnInit, OnDestroy {
|
|||
readonly vm = this;
|
||||
loading = false;
|
||||
exporting = false;
|
||||
exportingTemplate = false;
|
||||
errorMessage = '';
|
||||
toastOpen = false;
|
||||
toastMessage = '';
|
||||
|
|
@ -162,8 +160,7 @@ export class Parcelamentos implements OnInit, OnDestroy {
|
|||
constructor(
|
||||
private parcelamentosService: ParcelamentosService,
|
||||
private authService: AuthService,
|
||||
private tableExportService: TableExportService,
|
||||
private importPageTemplateService: ImportPageTemplateService
|
||||
private tableExportService: TableExportService
|
||||
) {}
|
||||
|
||||
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 {
|
||||
this.pageSize = size;
|
||||
this.page = 1;
|
||||
|
|
@ -1176,19 +1159,6 @@ export class Parcelamentos implements OnInit, OnDestroy {
|
|||
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 {
|
||||
if (value === null || value === undefined) return null;
|
||||
if (typeof value === 'number') return Number.isFinite(value) ? value : null;
|
||||
|
|
|
|||
|
|
@ -35,10 +35,6 @@
|
|||
<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>
|
||||
</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">
|
||||
<i class="bi bi-plus-circle me-1"></i> Nova Troca
|
||||
</button>
|
||||
|
|
|
|||
|
|
@ -14,7 +14,6 @@ import { firstValueFrom } from 'rxjs';
|
|||
import { AuthService } from '../../services/auth.service';
|
||||
import { CustomSelectComponent } from '../../components/custom-select/custom-select';
|
||||
import { TableExportService } from '../../services/table-export.service';
|
||||
import { ImportPageTemplateService } from '../../services/import-page-template.service';
|
||||
import { environment } from '../../../environments/environment';
|
||||
import { TrocaNumeroModalsComponent } from '../../components/page-modals/troca-numero-modals/troca-numero-modals';
|
||||
import {
|
||||
|
|
@ -78,7 +77,6 @@ export class TrocaNumero implements AfterViewInit {
|
|||
toastMessage = '';
|
||||
loading = false;
|
||||
exporting = false;
|
||||
exportingTemplate = false;
|
||||
|
||||
@ViewChild('successToast', { static: false }) successToast!: ElementRef;
|
||||
|
||||
|
|
@ -87,8 +85,7 @@ export class TrocaNumero implements AfterViewInit {
|
|||
private http: HttpClient,
|
||||
private cdr: ChangeDetectorRef,
|
||||
private authService: AuthService,
|
||||
private tableExportService: TableExportService,
|
||||
private importPageTemplateService: ImportPageTemplateService
|
||||
private tableExportService: TableExportService
|
||||
) {}
|
||||
|
||||
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[]> {
|
||||
const pageSize = 2000;
|
||||
let page = 1;
|
||||
|
|
|
|||
|
|
@ -28,10 +28,6 @@
|
|||
<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>
|
||||
</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()">
|
||||
<i class="bi bi-plus-circle me-1"></i> Nova Vigência
|
||||
</button>
|
||||
|
|
|
|||
|
|
@ -10,7 +10,6 @@ import { AuthService } from '../../services/auth.service';
|
|||
import { LinesService, MobileLineDetail } from '../../services/lines.service';
|
||||
import { PlanAutoFillService } from '../../services/plan-autofill.service';
|
||||
import { TableExportService } from '../../services/table-export.service';
|
||||
import { ImportPageTemplateService } from '../../services/import-page-template.service';
|
||||
import { confirmDeletionWithTyping } from '../../utils/destructive-confirmation';
|
||||
import { computeTotalPages } from '../../utils/pagination.util';
|
||||
import { VigenciaModalsComponent } from '../../components/page-modals/vigencia-modals/vigencia-modals';
|
||||
|
|
@ -38,7 +37,6 @@ export class VigenciaComponent implements OnInit, OnDestroy {
|
|||
readonly vm = this;
|
||||
loading = false;
|
||||
exporting = false;
|
||||
exportingTemplate = false;
|
||||
errorMsg = '';
|
||||
|
||||
// Filtros
|
||||
|
|
@ -121,8 +119,7 @@ export class VigenciaComponent implements OnInit, OnDestroy {
|
|||
private linesService: LinesService,
|
||||
private planAutoFill: PlanAutoFillService,
|
||||
private route: ActivatedRoute,
|
||||
private tableExportService: TableExportService,
|
||||
private importPageTemplateService: ImportPageTemplateService
|
||||
private tableExportService: TableExportService
|
||||
) {}
|
||||
|
||||
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[]> {
|
||||
const pageSize = 500;
|
||||
let page = 1;
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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] ?? '',
|
||||
};
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -19,19 +19,9 @@ export interface TableExportRequest<T> {
|
|||
sheetName?: string;
|
||||
columns: TableExportColumn<T>[];
|
||||
rows: T[];
|
||||
headerRows?: Array<Array<string | number | boolean | Date | null | undefined>>;
|
||||
excludeItemAndIdColumns?: boolean;
|
||||
templateBuffer?: ArrayBuffer | null;
|
||||
}
|
||||
|
||||
type ExcelJsModule = typeof import('exceljs');
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
ExcelJS?: ExcelJsModule;
|
||||
}
|
||||
}
|
||||
|
||||
type CellStyleSnapshot = {
|
||||
font?: Partial<import('exceljs').Font>;
|
||||
fill?: import('exceljs').Fill;
|
||||
|
|
@ -48,54 +38,43 @@ type TemplateStyleSnapshot = {
|
|||
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class TableExportService {
|
||||
private readonly excelJsAssetPath = 'vendor/exceljs.min.js?v=20260312-1';
|
||||
private readonly templatesApiBase = (() => {
|
||||
const apiBase = buildApiBaseUrl(environment.apiUrl);
|
||||
return `${apiBase}/templates`;
|
||||
})();
|
||||
private defaultTemplateBufferPromise: Promise<ArrayBuffer | null> | null = null;
|
||||
private cachedDefaultTemplateStyle?: TemplateStyleSnapshot;
|
||||
private excelJsPromise: Promise<ExcelJsModule> | null = null;
|
||||
|
||||
constructor(private readonly http: HttpClient) {}
|
||||
|
||||
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 templateStyle = await this.resolveTemplateStyle(excelJsModule, templateBuffer);
|
||||
const workbook = new excelJsModule.Workbook();
|
||||
const templateStyle = await this.resolveTemplateStyle(ExcelJS, templateBuffer);
|
||||
const workbook = new ExcelJS.Workbook();
|
||||
const sheet = workbook.addWorksheet(this.sanitizeSheetName(request.sheetName || 'Dados'));
|
||||
|
||||
const rawColumns = request.columns ?? [];
|
||||
const excludeItemAndIdColumns = request.excludeItemAndIdColumns ?? true;
|
||||
const columns = excludeItemAndIdColumns
|
||||
? rawColumns.filter((column) => !this.shouldExcludeColumnByHeader(column.header))
|
||||
: rawColumns;
|
||||
const columns = rawColumns.filter((column) => !this.shouldExcludeColumnByHeader(column.header));
|
||||
const rows = request.rows ?? [];
|
||||
const explicitHeaderRows = request.headerRows ?? [];
|
||||
|
||||
if (!columns.length) {
|
||||
throw new Error('Nenhuma coluna exportavel apos remover ITEM/ID.');
|
||||
}
|
||||
|
||||
const headerRows = explicitHeaderRows.length
|
||||
? explicitHeaderRows.map((row) => columns.map((_, columnIndex) => row[columnIndex] ?? ''))
|
||||
: [columns.map((column) => column.header ?? '')];
|
||||
|
||||
headerRows.forEach((headerValues) => {
|
||||
const headerValues = columns.map((c) => c.header ?? '');
|
||||
sheet.addRow(headerValues);
|
||||
});
|
||||
|
||||
rows.forEach((row, rowIndex) => {
|
||||
const values = columns.map((column) => this.normalizeValue(column.value(row, rowIndex), column.type));
|
||||
sheet.addRow(values);
|
||||
});
|
||||
|
||||
this.applyHeaderStyle(sheet, columns.length, headerRows.length, templateStyle);
|
||||
this.applyBodyStyle(sheet, columns, rows.length, headerRows.length, templateStyle);
|
||||
this.applyColumnWidths(sheet, columns, rows, headerRows, templateStyle);
|
||||
this.applyAutoFilter(sheet, columns.length, headerRows.length);
|
||||
sheet.views = [{ state: 'frozen', ySplit: headerRows.length }];
|
||||
this.applyHeaderStyle(sheet, columns.length, templateStyle);
|
||||
this.applyBodyStyle(sheet, columns, rows.length, templateStyle);
|
||||
this.applyColumnWidths(sheet, columns, rows, templateStyle);
|
||||
this.applyAutoFilter(sheet, columns.length);
|
||||
sheet.views = [{ state: 'frozen', ySplit: 1 }];
|
||||
|
||||
const extensionSafeName = this.ensureXlsxExtension(request.fileName);
|
||||
const buffer = await workbook.xlsx.writeBuffer();
|
||||
|
|
@ -114,11 +93,9 @@ export class TableExportService {
|
|||
private applyHeaderStyle(
|
||||
sheet: import('exceljs').Worksheet,
|
||||
columnCount: number,
|
||||
headerRowCount: number,
|
||||
templateStyle?: TemplateStyleSnapshot,
|
||||
): void {
|
||||
for (let rowIndex = 1; rowIndex <= headerRowCount; rowIndex += 1) {
|
||||
const headerRow = sheet.getRow(rowIndex);
|
||||
const headerRow = sheet.getRow(1);
|
||||
headerRow.height = 24;
|
||||
|
||||
for (let col = 1; col <= columnCount; col += 1) {
|
||||
|
|
@ -134,22 +111,16 @@ export class TableExportService {
|
|||
cell.border = this.cloneStyle(templateCell?.border) || this.getDefaultBorder();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private applyBodyStyle<T>(
|
||||
sheet: import('exceljs').Worksheet,
|
||||
columns: TableExportColumn<T>[],
|
||||
rowCount: number,
|
||||
headerRowCount: number,
|
||||
templateStyle?: TemplateStyleSnapshot,
|
||||
): void {
|
||||
const firstBodyRow = headerRowCount + 1;
|
||||
const lastBodyRow = headerRowCount + rowCount;
|
||||
|
||||
for (let rowIndex = firstBodyRow; rowIndex <= lastBodyRow; rowIndex += 1) {
|
||||
for (let rowIndex = 2; rowIndex <= rowCount + 1; rowIndex += 1) {
|
||||
const row = sheet.getRow(rowIndex);
|
||||
const bodyIndex = rowIndex - firstBodyRow;
|
||||
const isEven = bodyIndex % 2 === 1;
|
||||
const isEven = (rowIndex - 1) % 2 === 0;
|
||||
const templateRowStyle = isEven
|
||||
? (templateStyle?.bodyAltStyle ?? templateStyle?.bodyStyle)
|
||||
: (templateStyle?.bodyStyle ?? templateStyle?.bodyAltStyle);
|
||||
|
|
@ -183,7 +154,6 @@ export class TableExportService {
|
|||
sheet: import('exceljs').Worksheet,
|
||||
columns: TableExportColumn<T>[],
|
||||
rows: T[],
|
||||
headerRows: Array<Array<string | number | boolean | Date | null | undefined>>,
|
||||
templateStyle?: TemplateStyleSnapshot,
|
||||
): void {
|
||||
columns.forEach((column, columnIndex) => {
|
||||
|
|
@ -198,11 +168,7 @@ export class TableExportService {
|
|||
return;
|
||||
}
|
||||
|
||||
const explicitHeaderLength = headerRows.reduce((maxLength, headerRow) => {
|
||||
const current = String(headerRow[columnIndex] ?? '').length;
|
||||
return current > maxLength ? current : maxLength;
|
||||
}, 0);
|
||||
const headerLength = Math.max((column.header ?? '').length, explicitHeaderLength);
|
||||
const headerLength = (column.header ?? '').length;
|
||||
let maxLength = headerLength;
|
||||
|
||||
rows.forEach((row, rowIndex) => {
|
||||
|
|
@ -216,12 +182,11 @@ export class TableExportService {
|
|||
});
|
||||
}
|
||||
|
||||
private applyAutoFilter(sheet: import('exceljs').Worksheet, columnCount: number, headerRowCount: number): void {
|
||||
if (columnCount <= 0 || headerRowCount <= 0) return;
|
||||
const headerRow = headerRowCount;
|
||||
private applyAutoFilter(sheet: import('exceljs').Worksheet, columnCount: number): void {
|
||||
if (columnCount <= 0) return;
|
||||
sheet.autoFilter = {
|
||||
from: { row: headerRow, column: 1 },
|
||||
to: { row: headerRow, column: columnCount },
|
||||
from: { row: 1, column: 1 },
|
||||
to: { row: 1, column: columnCount },
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -392,71 +357,8 @@ export class TableExportService {
|
|||
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(
|
||||
excelJsModule: ExcelJsModule,
|
||||
excelJsModule: typeof import('exceljs'),
|
||||
templateBuffer: ArrayBuffer | null,
|
||||
): Promise<TemplateStyleSnapshot | undefined> {
|
||||
if (!templateBuffer) return undefined;
|
||||
|
|
@ -485,7 +387,7 @@ export class TableExportService {
|
|||
}
|
||||
|
||||
private async resolveTemplateStyle(
|
||||
excelJsModule: ExcelJsModule,
|
||||
excelJsModule: typeof import('exceljs'),
|
||||
templateBuffer: ArrayBuffer | null,
|
||||
): Promise<TemplateStyleSnapshot | undefined> {
|
||||
if (templateBuffer) {
|
||||
|
|
|
|||
Loading…
Reference in New Issue