Ajusta exportacoes e modelos das telas
This commit is contained in:
parent
af49f8265c
commit
ccea2fe13b
10
angular.json
10
angular.json
|
|
@ -28,6 +28,11 @@
|
||||||
{
|
{
|
||||||
"glob": "**/*",
|
"glob": "**/*",
|
||||||
"input": "public"
|
"input": "public"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"glob": "exceljs.min.js",
|
||||||
|
"input": "node_modules/exceljs/dist",
|
||||||
|
"output": "/vendor"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"styles": [
|
"styles": [
|
||||||
|
|
@ -99,6 +104,11 @@
|
||||||
{
|
{
|
||||||
"glob": "**/*",
|
"glob": "**/*",
|
||||||
"input": "public"
|
"input": "public"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"glob": "exceljs.min.js",
|
||||||
|
"input": "node_modules/exceljs/dist",
|
||||||
|
"output": "/vendor"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"styles": [
|
"styles": [
|
||||||
|
|
|
||||||
|
|
@ -43,6 +43,14 @@
|
||||||
<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"
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ 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,
|
||||||
|
|
@ -98,6 +99,7 @@ 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;
|
||||||
|
|
@ -137,7 +139,8 @@ 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 {
|
||||||
|
|
@ -507,6 +510,26 @@ 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 [];
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -36,6 +36,10 @@
|
||||||
<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>
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ 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,
|
||||||
|
|
@ -57,6 +58,7 @@ export class DadosUsuarios implements OnInit {
|
||||||
|
|
||||||
loading = false;
|
loading = false;
|
||||||
exporting = false;
|
exporting = false;
|
||||||
|
exportingTemplate = false;
|
||||||
errorMsg = '';
|
errorMsg = '';
|
||||||
|
|
||||||
// Filtros
|
// Filtros
|
||||||
|
|
@ -129,7 +131,8 @@ 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 {
|
||||||
|
|
@ -309,6 +312,20 @@ 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;
|
||||||
|
|
|
||||||
|
|
@ -38,6 +38,10 @@
|
||||||
<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>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -27,6 +27,7 @@ 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,
|
||||||
|
|
@ -67,11 +68,13 @@ 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 = '';
|
||||||
|
|
@ -465,6 +468,20 @@ 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));
|
||||||
|
|
|
||||||
|
|
@ -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 d-flex gap-2 justify-content-end align-items-start" data-animate>
|
<div class="header-actions" data-animate>
|
||||||
<div class="d-flex flex-column gap-2 align-items-start" *ngIf="isSysAdmin; else exportOnlyTpl">
|
<div class="header-actions-stack" *ngIf="isSysAdmin; else exportOnlyTpl">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="btn btn-glass btn-sm"
|
class="btn btn-glass btn-sm header-action-btn header-action-btn-wide"
|
||||||
(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 align-self-center"
|
class="btn btn-glass btn-sm header-action-btn header-action-btn-export"
|
||||||
(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,28 +51,41 @@
|
||||||
</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"
|
class="btn btn-glass btn-sm header-action-btn header-action-btn-export"
|
||||||
(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>
|
||||||
|
|
||||||
<input #excelInput type="file" class="d-none" accept=".xlsx" (change)="onExcelSelected($event)" />
|
<div class="header-actions-secondary">
|
||||||
<input #batchExcelInput type="file" class="d-none" accept=".xlsx" (change)="onBatchExcelSelected($event)" />
|
<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>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="btn btn-brand btn-sm align-self-start"
|
class="btn btn-brand btn-sm header-action-btn"
|
||||||
*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>
|
||||||
|
|
@ -99,17 +112,16 @@
|
||||||
<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">
|
<div class="filter-blocked-select-box">
|
||||||
<i class="bi bi-slash-circle blocked-status-select-icon" aria-hidden="true"></i>
|
<app-select
|
||||||
<select
|
class="select-glass"
|
||||||
class="blocked-status-native-select"
|
size="sm"
|
||||||
[value]="blockedStatusMode"
|
[options]="blockedStatusFilterOptions"
|
||||||
(change)="onBlockedStatusSelectChange($event)"
|
labelKey="label"
|
||||||
|
valueKey="value"
|
||||||
|
[ngModel]="blockedStatusSelectValue"
|
||||||
|
(ngModelChange)="onBlockedStatusSelectChange($event)"
|
||||||
[disabled]="loading"
|
[disabled]="loading"
|
||||||
>
|
></app-select>
|
||||||
<option *ngFor="let option of blockedStatusFilterOptions" [value]="option.value">
|
|
||||||
{{ option.label }}
|
|
||||||
</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -111,12 +111,107 @@
|
||||||
/* 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 { 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; } } }
|
.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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
.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 { justify-self: end; }
|
.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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/* 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; } }
|
||||||
|
|
@ -153,49 +248,6 @@
|
||||||
.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-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-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; }
|
.filter-blocked-select-box { width: 196px; flex: 0 0 auto; position: relative; z-index: 80; }
|
||||||
.blocked-status-select-icon {
|
|
||||||
position: absolute;
|
|
||||||
left: 11px;
|
|
||||||
top: 50%;
|
|
||||||
transform: translateY(-50%);
|
|
||||||
z-index: 2;
|
|
||||||
pointer-events: none;
|
|
||||||
color: rgba(17, 18, 20, 0.68);
|
|
||||||
font-size: 0.82rem;
|
|
||||||
}
|
|
||||||
.blocked-status-native-select {
|
|
||||||
width: 100%;
|
|
||||||
height: 36px;
|
|
||||||
border-radius: 12px;
|
|
||||||
border: 1px solid rgba(17, 18, 20, 0.15);
|
|
||||||
background: rgba(255, 255, 255, 0.7);
|
|
||||||
color: #0f172a;
|
|
||||||
font-size: 0.78rem;
|
|
||||||
font-weight: 800;
|
|
||||||
padding: 0 30px 0 30px;
|
|
||||||
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.04);
|
|
||||||
appearance: none;
|
|
||||||
-webkit-appearance: none;
|
|
||||||
transition: border-color 0.2s ease, box-shadow 0.2s ease, background-color 0.2s ease;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
background: #fff;
|
|
||||||
border-color: rgba(17, 18, 20, 0.7);
|
|
||||||
box-shadow: 0 4px 12px rgba(3, 15, 170, 0.1);
|
|
||||||
}
|
|
||||||
|
|
||||||
&:focus {
|
|
||||||
outline: none;
|
|
||||||
border-color: #e33dcf;
|
|
||||||
box-shadow: 0 0 0 3px rgba(227, 61, 207, 0.15);
|
|
||||||
}
|
|
||||||
|
|
||||||
&:disabled {
|
|
||||||
background-color: #f1f5f9;
|
|
||||||
color: #94a3b8;
|
|
||||||
cursor: not-allowed;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 1366px) {
|
@media (max-width: 1366px) {
|
||||||
.filter-tabs {
|
.filter-tabs {
|
||||||
|
|
|
||||||
|
|
@ -24,6 +24,7 @@ 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,
|
||||||
|
|
@ -69,6 +70,7 @@ 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' | 'PRE_ATIVACAO';
|
||||||
|
type BlockedStatusFilterValue = '' | BlockedStatusMode;
|
||||||
type SkilFilterMode = 'ALL' | 'PF' | 'PJ' | 'RESERVA' | 'ESTOQUE';
|
type SkilFilterMode = 'ALL' | 'PF' | 'PJ' | 'RESERVA' | 'ESTOQUE';
|
||||||
|
|
||||||
interface LineRow {
|
interface LineRow {
|
||||||
|
|
@ -391,6 +393,7 @@ 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 dropdownCoordinator: DropdownCoordinatorService
|
||||||
) {}
|
) {}
|
||||||
|
|
@ -399,6 +402,7 @@ export class Geral implements OnInit, AfterViewInit, OnDestroy {
|
||||||
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;
|
||||||
|
|
@ -418,7 +422,8 @@ export class Geral implements OnInit, AfterViewInit, OnDestroy {
|
||||||
selectedAdditionalServices: AdditionalServiceKey[] = [];
|
selectedAdditionalServices: AdditionalServiceKey[] = [];
|
||||||
filterOperadora: OperadoraFilterMode = 'ALL';
|
filterOperadora: OperadoraFilterMode = 'ALL';
|
||||||
filterContaEmpresa = '';
|
filterContaEmpresa = '';
|
||||||
readonly blockedStatusFilterOptions: Array<{ label: string; value: BlockedStatusMode }> = [
|
readonly blockedStatusFilterOptions: Array<{ label: string; value: BlockedStatusFilterValue }> = [
|
||||||
|
{ label: 'Todos os status', value: '' },
|
||||||
{ label: 'Bloqueadas', value: 'ALL' },
|
{ label: 'Bloqueadas', value: 'ALL' },
|
||||||
{ label: 'Bloqueio Perda/Roubo', value: 'PERDA_ROUBO' },
|
{ label: 'Bloqueio Perda/Roubo', value: 'PERDA_ROUBO' },
|
||||||
{ label: 'Bloqueio por 120 dias', value: 'BLOQUEIO_120' },
|
{ label: 'Bloqueio por 120 dias', value: 'BLOQUEIO_120' },
|
||||||
|
|
@ -722,6 +727,10 @@ 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
|
||||||
|
|
@ -1858,8 +1867,27 @@ export class Geral implements OnInit, AfterViewInit, OnDestroy {
|
||||||
this.refreshData();
|
this.refreshData();
|
||||||
}
|
}
|
||||||
|
|
||||||
setBlockedStatusFilter(mode: BlockedStatusMode) {
|
setBlockedStatusFilter(mode: BlockedStatusFilterValue) {
|
||||||
const normalizedMode = mode ?? 'ALL';
|
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;
|
const sameSelection = this.filterStatus === 'BLOCKED' && this.blockedStatusMode === normalizedMode;
|
||||||
|
|
||||||
if (sameSelection) {
|
if (sameSelection) {
|
||||||
|
|
@ -1884,10 +1912,8 @@ export class Geral implements OnInit, AfterViewInit, OnDestroy {
|
||||||
this.refreshData();
|
this.refreshData();
|
||||||
}
|
}
|
||||||
|
|
||||||
onBlockedStatusSelectChange(event: Event) {
|
onBlockedStatusSelectChange(value: BlockedStatusFilterValue) {
|
||||||
const target = event.target;
|
this.setBlockedStatusFilter(value);
|
||||||
const value = target instanceof HTMLSelectElement ? target.value : 'ALL';
|
|
||||||
this.setBlockedStatusFilter(value as BlockedStatusMode);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
setAdditionalMode(mode: AdditionalMode) {
|
setAdditionalMode(mode: AdditionalMode) {
|
||||||
|
|
@ -2801,6 +2827,20 @@ 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();
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -35,6 +35,10 @@
|
||||||
<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>
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,7 @@ 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';
|
||||||
|
|
@ -109,6 +110,7 @@ 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;
|
||||||
|
|
||||||
|
|
@ -118,7 +120,8 @@ 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');
|
||||||
|
|
@ -264,6 +267,20 @@ 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;
|
||||||
|
|
|
||||||
|
|
@ -29,6 +29,10 @@
|
||||||
<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>
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ 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,
|
||||||
|
|
@ -68,6 +69,7 @@ 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 = '';
|
||||||
|
|
@ -160,7 +162,8 @@ 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 {
|
||||||
|
|
@ -335,6 +338,20 @@ 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;
|
||||||
|
|
@ -1159,6 +1176,19 @@ 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;
|
||||||
|
|
|
||||||
|
|
@ -35,6 +35,10 @@
|
||||||
<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>
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,7 @@ 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 {
|
||||||
|
|
@ -77,6 +78,7 @@ 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;
|
||||||
|
|
||||||
|
|
@ -85,7 +87,8 @@ 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');
|
||||||
|
|
@ -213,6 +216,20 @@ 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;
|
||||||
|
|
|
||||||
|
|
@ -28,6 +28,10 @@
|
||||||
<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>
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@ 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';
|
||||||
|
|
@ -37,6 +38,7 @@ 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
|
||||||
|
|
@ -119,7 +121,8 @@ 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 {
|
||||||
|
|
@ -347,6 +350,20 @@ 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;
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,296 @@
|
||||||
|
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,9 +19,19 @@ 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;
|
||||||
|
|
@ -38,43 +48,54 @@ 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 ExcelJS = await import('exceljs');
|
const excelJsModule = await this.getExcelJs();
|
||||||
const templateBuffer = request.templateBuffer ?? (await this.getDefaultTemplateBuffer());
|
const templateBuffer = request.templateBuffer ?? (await this.getDefaultTemplateBuffer());
|
||||||
const templateStyle = await this.resolveTemplateStyle(ExcelJS, templateBuffer);
|
const templateStyle = await this.resolveTemplateStyle(excelJsModule, templateBuffer);
|
||||||
const workbook = new ExcelJS.Workbook();
|
const workbook = new excelJsModule.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 columns = rawColumns.filter((column) => !this.shouldExcludeColumnByHeader(column.header));
|
const excludeItemAndIdColumns = request.excludeItemAndIdColumns ?? true;
|
||||||
|
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 headerValues = columns.map((c) => c.header ?? '');
|
const headerRows = explicitHeaderRows.length
|
||||||
|
? 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, templateStyle);
|
this.applyHeaderStyle(sheet, columns.length, headerRows.length, templateStyle);
|
||||||
this.applyBodyStyle(sheet, columns, rows.length, templateStyle);
|
this.applyBodyStyle(sheet, columns, rows.length, headerRows.length, templateStyle);
|
||||||
this.applyColumnWidths(sheet, columns, rows, templateStyle);
|
this.applyColumnWidths(sheet, columns, rows, headerRows, templateStyle);
|
||||||
this.applyAutoFilter(sheet, columns.length);
|
this.applyAutoFilter(sheet, columns.length, headerRows.length);
|
||||||
sheet.views = [{ state: 'frozen', ySplit: 1 }];
|
sheet.views = [{ state: 'frozen', ySplit: headerRows.length }];
|
||||||
|
|
||||||
const extensionSafeName = this.ensureXlsxExtension(request.fileName);
|
const extensionSafeName = this.ensureXlsxExtension(request.fileName);
|
||||||
const buffer = await workbook.xlsx.writeBuffer();
|
const buffer = await workbook.xlsx.writeBuffer();
|
||||||
|
|
@ -93,9 +114,11 @@ 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 {
|
||||||
const headerRow = sheet.getRow(1);
|
for (let rowIndex = 1; rowIndex <= headerRowCount; rowIndex += 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) {
|
||||||
|
|
@ -111,16 +134,22 @@ 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 {
|
||||||
for (let rowIndex = 2; rowIndex <= rowCount + 1; rowIndex += 1) {
|
const firstBodyRow = headerRowCount + 1;
|
||||||
|
const lastBodyRow = headerRowCount + rowCount;
|
||||||
|
|
||||||
|
for (let rowIndex = firstBodyRow; rowIndex <= lastBodyRow; rowIndex += 1) {
|
||||||
const row = sheet.getRow(rowIndex);
|
const row = sheet.getRow(rowIndex);
|
||||||
const isEven = (rowIndex - 1) % 2 === 0;
|
const bodyIndex = rowIndex - firstBodyRow;
|
||||||
|
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);
|
||||||
|
|
@ -154,6 +183,7 @@ 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) => {
|
||||||
|
|
@ -168,7 +198,11 @@ export class TableExportService {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const headerLength = (column.header ?? '').length;
|
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);
|
||||||
let maxLength = headerLength;
|
let maxLength = headerLength;
|
||||||
|
|
||||||
rows.forEach((row, rowIndex) => {
|
rows.forEach((row, rowIndex) => {
|
||||||
|
|
@ -182,11 +216,12 @@ export class TableExportService {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private applyAutoFilter(sheet: import('exceljs').Worksheet, columnCount: number): void {
|
private applyAutoFilter(sheet: import('exceljs').Worksheet, columnCount: number, headerRowCount: number): void {
|
||||||
if (columnCount <= 0) return;
|
if (columnCount <= 0 || headerRowCount <= 0) return;
|
||||||
|
const headerRow = headerRowCount;
|
||||||
sheet.autoFilter = {
|
sheet.autoFilter = {
|
||||||
from: { row: 1, column: 1 },
|
from: { row: headerRow, column: 1 },
|
||||||
to: { row: 1, column: columnCount },
|
to: { row: headerRow, column: columnCount },
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -357,8 +392,71 @@ 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: typeof import('exceljs'),
|
excelJsModule: ExcelJsModule,
|
||||||
templateBuffer: ArrayBuffer | null,
|
templateBuffer: ArrayBuffer | null,
|
||||||
): Promise<TemplateStyleSnapshot | undefined> {
|
): Promise<TemplateStyleSnapshot | undefined> {
|
||||||
if (!templateBuffer) return undefined;
|
if (!templateBuffer) return undefined;
|
||||||
|
|
@ -387,7 +485,7 @@ export class TableExportService {
|
||||||
}
|
}
|
||||||
|
|
||||||
private async resolveTemplateStyle(
|
private async resolveTemplateStyle(
|
||||||
excelJsModule: typeof import('exceljs'),
|
excelJsModule: ExcelJsModule,
|
||||||
templateBuffer: ArrayBuffer | null,
|
templateBuffer: ArrayBuffer | null,
|
||||||
): Promise<TemplateStyleSnapshot | undefined> {
|
): Promise<TemplateStyleSnapshot | undefined> {
|
||||||
if (templateBuffer) {
|
if (templateBuffer) {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue