Compare commits
2 Commits
31999870e4
...
2d7c4d4a0e
| Author | SHA1 | Date |
|---|---|---|
|
|
2d7c4d4a0e | |
|
|
5e3d085daf |
|
|
@ -6,6 +6,7 @@
|
|||
[attr.aria-expanded]="isOpen"
|
||||
[attr.aria-disabled]="disabled"
|
||||
>
|
||||
<i class="bi app-select-leading-icon" *ngIf="leadingIcon" [ngClass]="leadingIcon"></i>
|
||||
<span class="app-select-label">{{ displayLabel }}</span>
|
||||
<i class="bi bi-chevron-down"></i>
|
||||
</button>
|
||||
|
|
|
|||
|
|
@ -67,6 +67,12 @@
|
|||
padding-right: 24px;
|
||||
}
|
||||
|
||||
.app-select-leading-icon {
|
||||
flex: 0 0 auto;
|
||||
color: #64748b;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.app-select-label {
|
||||
flex: 1 1 auto;
|
||||
min-width: 0;
|
||||
|
|
|
|||
|
|
@ -29,6 +29,7 @@ export class CustomSelectComponent implements ControlValueAccessor, OnDestroy {
|
|||
@Input() disabled = false;
|
||||
@Input() searchable = false;
|
||||
@Input() searchPlaceholder = 'Pesquisar...';
|
||||
@Input() leadingIcon = '';
|
||||
|
||||
isOpen = false;
|
||||
value: any = null;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,28 @@
|
|||
<div class="input-group input-group-sm smart-search-group">
|
||||
<span class="input-group-text">
|
||||
<i
|
||||
class="bi"
|
||||
[class.bi-search]="!loading"
|
||||
[class.bi-hourglass-split]="loading"
|
||||
[class.text-brand]="loading"></i>
|
||||
</span>
|
||||
|
||||
<input
|
||||
class="form-control"
|
||||
type="text"
|
||||
[placeholder]="placeholder"
|
||||
[value]="value"
|
||||
[disabled]="disabled"
|
||||
autocomplete="off"
|
||||
(input)="onInput($event)"
|
||||
(blur)="onBlur()" />
|
||||
|
||||
<button
|
||||
class="btn btn-outline-secondary btn-clear"
|
||||
type="button"
|
||||
[disabled]="disabled"
|
||||
*ngIf="value"
|
||||
(click)="clear($event)">
|
||||
<i class="bi bi-x-lg"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
|
@ -0,0 +1,72 @@
|
|||
:host {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.smart-search-group {
|
||||
width: 100%;
|
||||
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, #E33DCF);
|
||||
box-shadow: 0 4px 12px rgba(227, 61, 207, 0.15);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
}
|
||||
|
||||
.input-group-text {
|
||||
background: transparent;
|
||||
border: none;
|
||||
color: rgba(17, 18, 20, 0.65);
|
||||
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: #111214;
|
||||
box-shadow: none;
|
||||
|
||||
&::placeholder {
|
||||
color: rgba(17, 18, 20, 0.4);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
&:focus {
|
||||
outline: none;
|
||||
}
|
||||
}
|
||||
|
||||
.btn-clear {
|
||||
background: transparent;
|
||||
border: none;
|
||||
color: rgba(17, 18, 20, 0.65);
|
||||
padding: 0 12px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
transition: color 0.2s;
|
||||
|
||||
&:hover:not(:disabled) {
|
||||
color: #dc3545;
|
||||
}
|
||||
|
||||
i {
|
||||
font-size: 1rem;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,71 @@
|
|||
import { CommonModule } from '@angular/common';
|
||||
import { Component, HostBinding, Input, forwardRef } from '@angular/core';
|
||||
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
|
||||
|
||||
@Component({
|
||||
selector: 'app-smart-search-input',
|
||||
standalone: true,
|
||||
imports: [CommonModule],
|
||||
templateUrl: './smart-search-input.html',
|
||||
styleUrls: ['./smart-search-input.scss'],
|
||||
providers: [
|
||||
{
|
||||
provide: NG_VALUE_ACCESSOR,
|
||||
useExisting: forwardRef(() => SmartSearchInputComponent),
|
||||
multi: true,
|
||||
},
|
||||
],
|
||||
})
|
||||
export class SmartSearchInputComponent implements ControlValueAccessor {
|
||||
@Input() placeholder = 'Pesquisar...';
|
||||
@Input() loading = false;
|
||||
@Input() disabled = false;
|
||||
@Input() maxWidth = '270px';
|
||||
|
||||
@HostBinding('style.display') readonly hostDisplay = 'block';
|
||||
|
||||
@HostBinding('style.width')
|
||||
get hostWidth(): string {
|
||||
return this.maxWidth === '100%' ? '100%' : `min(100%, ${this.maxWidth})`;
|
||||
}
|
||||
|
||||
value = '';
|
||||
|
||||
private onChange: (value: string) => void = () => {};
|
||||
private onTouched: () => void = () => {};
|
||||
|
||||
writeValue(value: string | null | undefined): void {
|
||||
this.value = value ?? '';
|
||||
}
|
||||
|
||||
registerOnChange(fn: (value: string) => void): void {
|
||||
this.onChange = fn;
|
||||
}
|
||||
|
||||
registerOnTouched(fn: () => void): void {
|
||||
this.onTouched = fn;
|
||||
}
|
||||
|
||||
setDisabledState(isDisabled: boolean): void {
|
||||
this.disabled = isDisabled;
|
||||
}
|
||||
|
||||
onInput(event: Event): void {
|
||||
const nextValue = (event.target as HTMLInputElement | null)?.value ?? '';
|
||||
this.value = nextValue;
|
||||
this.onChange(nextValue);
|
||||
}
|
||||
|
||||
clear(event?: Event): void {
|
||||
event?.stopPropagation();
|
||||
if (!this.value) return;
|
||||
|
||||
this.value = '';
|
||||
this.onChange('');
|
||||
this.onTouched();
|
||||
}
|
||||
|
||||
onBlur(): void {
|
||||
this.onTouched();
|
||||
}
|
||||
}
|
||||
|
|
@ -46,31 +46,31 @@
|
|||
class="btn btn-glass btn-sm header-action-btn header-action-btn-export"
|
||||
(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>
|
||||
<span class="header-action-btn-content" *ngIf="!exporting"><i class="bi bi-download me-1"></i> Exportar</span>
|
||||
<span class="header-action-btn-content" *ngIf="exporting"><span class="spinner-border spinner-border-sm me-2"></span> Exportando...</span>
|
||||
</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"
|
||||
(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>
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-glass btn-sm header-action-btn header-action-btn-export"
|
||||
(click)="onExport()"
|
||||
[disabled]="loading || exporting">
|
||||
<span class="header-action-btn-content" *ngIf="!exporting"><i class="bi bi-download me-1"></i> Exportar</span>
|
||||
<span class="header-action-btn-content" *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"
|
||||
class="btn btn-glass btn-sm header-action-btn header-action-btn-template"
|
||||
(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>
|
||||
<span class="header-action-btn-content" *ngIf="!exportingTemplate"><i class="bi bi-file-earmark-arrow-down me-1"></i> Exportar Modelo</span>
|
||||
<span class="header-action-btn-content" *ngIf="exportingTemplate"><span class="spinner-border spinner-border-sm me-2"></span> Gerando Modelo...</span>
|
||||
</button>
|
||||
|
||||
<button
|
||||
|
|
@ -113,9 +113,11 @@
|
|||
</button>
|
||||
<div class="filter-blocked-select-box">
|
||||
<app-select
|
||||
class="select-glass"
|
||||
class="select-glass blocked-select-glass"
|
||||
size="sm"
|
||||
[options]="blockedStatusFilterOptions"
|
||||
placeholder="Bloqueio"
|
||||
leadingIcon="bi-lock"
|
||||
labelKey="label"
|
||||
valueKey="value"
|
||||
[ngModel]="blockedStatusSelectValue"
|
||||
|
|
@ -335,21 +337,12 @@
|
|||
|
||||
<!-- CONTROLS -->
|
||||
<div class="controls mt-3 mb-2" data-animate>
|
||||
<div class="input-group input-group-sm search-group">
|
||||
<span class="input-group-text">
|
||||
<i
|
||||
class="bi"
|
||||
[class.bi-search]="!loading"
|
||||
[class.bi-hourglass-split]="loading"
|
||||
[class.text-brand]="loading"></i>
|
||||
</span>
|
||||
|
||||
<input class="form-control" placeholder="Pesquisar..." [(ngModel)]="searchTerm" (ngModelChange)="onSearch()" />
|
||||
|
||||
<button class="btn btn-outline-secondary btn-clear" type="button" (click)="clearSearch()" *ngIf="searchTerm">
|
||||
<i class="bi bi-x-lg"></i>
|
||||
</button>
|
||||
</div>
|
||||
<app-smart-search-input
|
||||
[(ngModel)]="searchTerm"
|
||||
(ngModelChange)="onSearch()"
|
||||
[loading]="loading"
|
||||
placeholder="Pesquisar..."
|
||||
></app-smart-search-input>
|
||||
|
||||
<div class="controls-right">
|
||||
<div class="batch-status-tools" *ngIf="canManageLines">
|
||||
|
|
@ -460,6 +453,17 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div class="group-search-row">
|
||||
<app-smart-search-input
|
||||
class="group-search-input"
|
||||
[(ngModel)]="groupSearchTerm"
|
||||
(ngModelChange)="onGroupSearchChange()"
|
||||
[loading]="loadingLines"
|
||||
maxWidth="100%"
|
||||
placeholder="Pesquisar..."
|
||||
></app-smart-search-input>
|
||||
</div>
|
||||
|
||||
<!-- ✅ wrapper com classe extra para permitir MAIS ALTURA em notebook/TV via SCSS -->
|
||||
<div class="table-wrap inner-table-wrap table-wrap-responsive table-wrap-tall">
|
||||
<div *ngIf="loadingLines" class="p-4 text-center text-muted">
|
||||
|
|
|
|||
|
|
@ -173,16 +173,30 @@
|
|||
}
|
||||
|
||||
.header-action-btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 38px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.header-action-btn-content {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.header-action-btn-export {
|
||||
align-self: center;
|
||||
min-width: 152px;
|
||||
}
|
||||
|
||||
.header-action-btn-wide {
|
||||
min-width: 220px;
|
||||
}
|
||||
|
||||
.header-action-btn-export {
|
||||
align-self: center;
|
||||
min-width: 140px;
|
||||
.header-action-btn-template {
|
||||
min-width: 220px;
|
||||
}
|
||||
|
||||
@media (max-width: 1366px) {
|
||||
|
|
@ -206,8 +220,8 @@
|
|||
}
|
||||
|
||||
.header-action-btn,
|
||||
.header-action-btn-wide,
|
||||
.header-action-btn-export {
|
||||
.header-action-btn-export,
|
||||
.header-action-btn-wide {
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
}
|
||||
|
|
@ -247,7 +261,12 @@
|
|||
}
|
||||
.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; }
|
||||
.filter-blocked-select-box {
|
||||
width: 196px;
|
||||
flex: 0 0 auto;
|
||||
position: relative;
|
||||
z-index: 80;
|
||||
}
|
||||
|
||||
@media (max-width: 1366px) {
|
||||
.filter-tabs {
|
||||
|
|
@ -529,6 +548,15 @@
|
|||
.group-toggle-icon { font-size: 1.2rem; color: var(--muted); transition: transform 0.3s ease; }
|
||||
.client-group-card.expanded .group-toggle-icon { transform: rotate(180deg); color: var(--brand); }
|
||||
.group-body { border-top: 1px solid rgba(17,18,20,0.06); background: #fbfbfc; animation: slideDown 0.3s cubic-bezier(0.16, 1, 0.3, 1); }
|
||||
.group-search-row {
|
||||
padding: 14px 24px 0;
|
||||
background: rgba(255, 255, 255, 0.82);
|
||||
}
|
||||
|
||||
.group-search-input {
|
||||
width: min(100%, 420px);
|
||||
}
|
||||
|
||||
.btn-add-line-group {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ import {
|
|||
} from '@angular/common/http';
|
||||
import { ActivatedRoute, NavigationEnd, Router } from '@angular/router';
|
||||
import { CustomSelectComponent } from '../../components/custom-select/custom-select';
|
||||
import { SmartSearchInputComponent } from '../../components/smart-search-input/smart-search-input';
|
||||
import { GeralModalsComponent } from '../../components/page-modals/geral-modals/geral-modals';
|
||||
import { PlanAutoFillService } from '../../services/plan-autofill.service';
|
||||
import { AuthService } from '../../services/auth.service';
|
||||
|
|
@ -178,6 +179,8 @@ interface ApiLineDetail {
|
|||
|
||||
desconto?: number | null;
|
||||
lucro?: number | null;
|
||||
createdAt?: string | null;
|
||||
updatedAt?: string | null;
|
||||
}
|
||||
|
||||
type UpdateMobileLineRequest = Omit<ApiLineDetail, 'id'>;
|
||||
|
|
@ -359,7 +362,7 @@ interface MveApplySelectionSummary {
|
|||
|
||||
@Component({
|
||||
standalone: true,
|
||||
imports: [CommonModule, FormsModule, CustomSelectComponent, GeralModalsComponent],
|
||||
imports: [CommonModule, FormsModule, CustomSelectComponent, SmartSearchInputComponent, GeralModalsComponent],
|
||||
templateUrl: './geral.html',
|
||||
styleUrls: ['./geral.scss']
|
||||
})
|
||||
|
|
@ -415,6 +418,7 @@ export class Geral implements OnInit, AfterViewInit, OnDestroy {
|
|||
loadingLines = false;
|
||||
|
||||
searchTerm = '';
|
||||
groupSearchTerm = '';
|
||||
filterSkil: SkilFilterMode = 'ALL';
|
||||
filterStatus: 'ALL' | 'ACTIVE' | 'BLOCKED' = 'ALL';
|
||||
blockedStatusMode: BlockedStatusMode = 'ALL';
|
||||
|
|
@ -423,8 +427,7 @@ export class Geral implements OnInit, AfterViewInit, OnDestroy {
|
|||
filterOperadora: OperadoraFilterMode = 'ALL';
|
||||
filterContaEmpresa = '';
|
||||
readonly blockedStatusFilterOptions: Array<{ label: string; value: BlockedStatusFilterValue }> = [
|
||||
{ label: 'Todos os status', value: '' },
|
||||
{ label: 'Bloqueadas', value: 'ALL' },
|
||||
{ label: 'Todos os bloqueios', 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' },
|
||||
|
|
@ -526,6 +529,7 @@ export class Geral implements OnInit, AfterViewInit, OnDestroy {
|
|||
|
||||
private editingId: string | null = null;
|
||||
private searchTimer: any = null;
|
||||
private groupSearchTimer: any = null;
|
||||
private navigationSub?: Subscription;
|
||||
private dropdownSyncSub?: Subscription;
|
||||
private keepPageOnNextGroupsLoad = false;
|
||||
|
|
@ -1040,6 +1044,7 @@ export class Geral implements OnInit, AfterViewInit, OnDestroy {
|
|||
|
||||
ngOnDestroy(): void {
|
||||
if (this.searchTimer) clearTimeout(this.searchTimer);
|
||||
if (this.groupSearchTimer) clearTimeout(this.groupSearchTimer);
|
||||
this.navigationSub?.unsubscribe();
|
||||
this.dropdownSyncSub?.unsubscribe();
|
||||
}
|
||||
|
|
@ -1468,6 +1473,22 @@ export class Geral implements OnInit, AfterViewInit, OnDestroy {
|
|||
return false;
|
||||
}
|
||||
|
||||
private resetGroupSearchState(): void {
|
||||
this.groupSearchTerm = '';
|
||||
if (this.groupSearchTimer) {
|
||||
clearTimeout(this.groupSearchTimer);
|
||||
this.groupSearchTimer = null;
|
||||
}
|
||||
}
|
||||
|
||||
private getActiveExpandedGroupSearchTerm(): string | undefined {
|
||||
const groupTerm = (this.groupSearchTerm ?? '').trim();
|
||||
if (groupTerm) return groupTerm;
|
||||
|
||||
const globalTerm = (this.searchTerm ?? '').trim();
|
||||
return globalTerm || undefined;
|
||||
}
|
||||
|
||||
private normalizeDigits(value: unknown): string {
|
||||
return String(value ?? '').replace(/\D/g, '');
|
||||
}
|
||||
|
|
@ -1632,6 +1653,7 @@ export class Geral implements OnInit, AfterViewInit, OnDestroy {
|
|||
this.searchTimer = setTimeout(async () => {
|
||||
const requestVersion = ++this.searchRequestVersion;
|
||||
|
||||
this.resetGroupSearchState();
|
||||
this.expandedGroup = null;
|
||||
this.groupLines = [];
|
||||
this.page = 1;
|
||||
|
|
@ -1679,6 +1701,15 @@ export class Geral implements OnInit, AfterViewInit, OnDestroy {
|
|||
}, 300);
|
||||
}
|
||||
|
||||
onGroupSearchChange(): void {
|
||||
if (this.groupSearchTimer) clearTimeout(this.groupSearchTimer);
|
||||
|
||||
this.groupSearchTimer = setTimeout(() => {
|
||||
if (!this.expandedGroup) return;
|
||||
this.fetchGroupLines(this.expandedGroup, this.getActiveExpandedGroupSearchTerm());
|
||||
}, 300);
|
||||
}
|
||||
|
||||
private loadOnlyThisClientGroup(clientName: string): Promise<void> {
|
||||
const requestVersion = ++this.groupsRequestVersion;
|
||||
this.loading = true;
|
||||
|
|
@ -2485,18 +2516,17 @@ export class Geral implements OnInit, AfterViewInit, OnDestroy {
|
|||
|
||||
toggleGroup(clientName: string) {
|
||||
if (this.expandedGroup === clientName) {
|
||||
this.resetGroupSearchState();
|
||||
this.expandedGroup = null;
|
||||
this.groupLines = [];
|
||||
this.clearReservaSelection();
|
||||
return;
|
||||
}
|
||||
|
||||
this.resetGroupSearchState();
|
||||
this.clearReservaSelection();
|
||||
this.expandedGroup = clientName;
|
||||
|
||||
const term = (this.searchTerm ?? '').trim();
|
||||
const useTerm = term && this.isSpecificSearchTerm(term) ? term : undefined;
|
||||
|
||||
this.fetchGroupLines(clientName, useTerm);
|
||||
this.fetchGroupLines(clientName, this.getActiveExpandedGroupSearchTerm());
|
||||
}
|
||||
|
||||
private async fetchAllGroupLines(
|
||||
|
|
@ -2582,8 +2612,9 @@ export class Geral implements OnInit, AfterViewInit, OnDestroy {
|
|||
this.groupLines = [];
|
||||
this.clearReservaSelection();
|
||||
this.loadingLines = true;
|
||||
const normalizedSearch = (search ?? '').trim() || undefined;
|
||||
|
||||
void this.fetchAllGroupLines(clientName, search, requestVersion);
|
||||
void this.fetchAllGroupLines(clientName, normalizedSearch, requestVersion);
|
||||
}
|
||||
|
||||
toggleClientMenu() {
|
||||
|
|
@ -2740,6 +2771,7 @@ export class Geral implements OnInit, AfterViewInit, OnDestroy {
|
|||
clearSearch() {
|
||||
this.searchTerm = '';
|
||||
this.searchResolvedClient = null;
|
||||
this.resetGroupSearchState();
|
||||
this.expandedGroup = null;
|
||||
this.groupLines = [];
|
||||
this.page = 1;
|
||||
|
|
@ -2761,12 +2793,10 @@ export class Geral implements OnInit, AfterViewInit, OnDestroy {
|
|||
const suffix = this.getExportFilterSuffix();
|
||||
const timestamp = this.tableExportService.buildTimestamp();
|
||||
const fileName = `geral_${suffix}_${timestamp}`;
|
||||
const templateBuffer = await this.getGeralTemplateBuffer();
|
||||
|
||||
await this.tableExportService.exportAsXlsx<ApiLineDetail>({
|
||||
fileName,
|
||||
sheetName: 'Geral',
|
||||
templateBuffer,
|
||||
rows,
|
||||
columns: [
|
||||
{ header: 'ID', value: (row) => row.id },
|
||||
|
|
@ -2882,29 +2912,27 @@ export class Geral implements OnInit, AfterViewInit, OnDestroy {
|
|||
private async getDetailedRowsForExport(baseRows: LineRow[]): Promise<ApiLineDetail[]> {
|
||||
if (!baseRows.length) return [];
|
||||
|
||||
const result: ApiLineDetail[] = [];
|
||||
const chunkSize = 8;
|
||||
|
||||
for (let i = 0; i < baseRows.length; i += chunkSize) {
|
||||
const chunk = baseRows.slice(i, i + chunkSize);
|
||||
const fetched = await Promise.all(
|
||||
chunk.map(async (row) => {
|
||||
try {
|
||||
return await firstValueFrom(
|
||||
this.http.get<ApiLineDetail>(`${this.apiBase}/${row.id}`, {
|
||||
params: this.withNoCache(new HttpParams()),
|
||||
})
|
||||
);
|
||||
} catch {
|
||||
return this.toDetailFallback(row);
|
||||
}
|
||||
})
|
||||
try {
|
||||
const ids = Array.from(
|
||||
new Set(
|
||||
baseRows
|
||||
.map((row) => (row.id ?? '').toString().trim())
|
||||
.filter((id) => !!id)
|
||||
)
|
||||
);
|
||||
|
||||
result.push(...fetched);
|
||||
const fetched = await firstValueFrom(
|
||||
this.http.post<ApiLineDetail[]>(`${this.apiBase}/export-details`, { ids })
|
||||
);
|
||||
|
||||
if (Array.isArray(fetched) && fetched.length > 0) {
|
||||
return fetched;
|
||||
}
|
||||
} catch {
|
||||
// Fallback local preserva a exportacao se o endpoint em lote falhar.
|
||||
}
|
||||
|
||||
return result;
|
||||
return baseRows.map((row) => this.toDetailFallback(row));
|
||||
}
|
||||
|
||||
private toDetailFallback(row: LineRow): ApiLineDetail {
|
||||
|
|
@ -2954,24 +2982,11 @@ export class Geral implements OnInit, AfterViewInit, OnDestroy {
|
|||
valorContratoLine: null,
|
||||
desconto: null,
|
||||
lucro: null,
|
||||
createdAt: null,
|
||||
updatedAt: null,
|
||||
};
|
||||
}
|
||||
|
||||
private async getGeralTemplateBuffer(): Promise<ArrayBuffer | null> {
|
||||
try {
|
||||
const params = new HttpParams().set('_', `${Date.now()}`);
|
||||
const blob = await firstValueFrom(
|
||||
this.http.get(`${this.templatesApiBase}/planilha-geral`, {
|
||||
params,
|
||||
responseType: 'blob',
|
||||
})
|
||||
);
|
||||
return await blob.arrayBuffer();
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private getExportFilterSuffix(): string {
|
||||
const parts: string[] = [];
|
||||
|
||||
|
|
@ -3488,9 +3503,7 @@ export class Geral implements OnInit, AfterViewInit, OnDestroy {
|
|||
await this.showToast(msg);
|
||||
|
||||
if (this.isGroupMode && this.expandedGroup) {
|
||||
const term = (this.searchTerm ?? '').trim();
|
||||
const useTerm = term && this.isSpecificSearchTerm(term) ? term : undefined;
|
||||
this.fetchGroupLines(this.expandedGroup, useTerm);
|
||||
this.fetchGroupLines(this.expandedGroup, this.getActiveExpandedGroupSearchTerm());
|
||||
this.loadGroups();
|
||||
this.loadKpis();
|
||||
} else {
|
||||
|
|
@ -3516,10 +3529,7 @@ export class Geral implements OnInit, AfterViewInit, OnDestroy {
|
|||
}
|
||||
|
||||
if (this.isGroupMode && this.expandedGroup) {
|
||||
const term = (this.searchTerm ?? '').trim();
|
||||
const useTerm = term && this.isSpecificSearchTerm(term) ? term : undefined;
|
||||
|
||||
this.fetchGroupLines(this.expandedGroup, useTerm);
|
||||
this.fetchGroupLines(this.expandedGroup, this.getActiveExpandedGroupSearchTerm());
|
||||
this.loadGroups();
|
||||
this.loadKpis();
|
||||
} else {
|
||||
|
|
@ -3683,10 +3693,7 @@ export class Geral implements OnInit, AfterViewInit, OnDestroy {
|
|||
this.loading = false;
|
||||
|
||||
if (fromGroup && this.expandedGroup) {
|
||||
const term = (this.searchTerm ?? '').trim();
|
||||
const useTerm = term && this.isSpecificSearchTerm(term) ? term : undefined;
|
||||
|
||||
this.fetchGroupLines(this.expandedGroup, useTerm);
|
||||
this.fetchGroupLines(this.expandedGroup, this.getActiveExpandedGroupSearchTerm());
|
||||
this.loadGroups();
|
||||
this.loadKpis();
|
||||
} else {
|
||||
|
|
@ -4480,10 +4487,7 @@ export class Geral implements OnInit, AfterViewInit, OnDestroy {
|
|||
await this.showToast(this.getCreateSuccessMessage(createdCount));
|
||||
|
||||
if (this.createMode === 'NEW_LINE_IN_GROUP' && this.expandedGroup === targetClient) {
|
||||
const term = (this.searchTerm ?? '').trim();
|
||||
const useTerm = term && this.isSpecificSearchTerm(term) ? term : undefined;
|
||||
|
||||
this.fetchGroupLines(this.expandedGroup!, useTerm);
|
||||
this.fetchGroupLines(this.expandedGroup!, this.getActiveExpandedGroupSearchTerm());
|
||||
this.loadGroups();
|
||||
this.loadKpis();
|
||||
} else {
|
||||
|
|
@ -4762,9 +4766,7 @@ export class Geral implements OnInit, AfterViewInit, OnDestroy {
|
|||
);
|
||||
|
||||
if (this.expandedGroup) {
|
||||
const term = (this.searchTerm ?? '').trim();
|
||||
const useTerm = term && this.isSpecificSearchTerm(term) ? term : undefined;
|
||||
this.fetchGroupLines(this.expandedGroup, useTerm);
|
||||
this.fetchGroupLines(this.expandedGroup, this.getActiveExpandedGroupSearchTerm());
|
||||
}
|
||||
|
||||
this.loadGroups();
|
||||
|
|
@ -4923,9 +4925,7 @@ export class Geral implements OnInit, AfterViewInit, OnDestroy {
|
|||
);
|
||||
|
||||
if (this.expandedGroup) {
|
||||
const term = (this.searchTerm ?? '').trim();
|
||||
const useTerm = term && this.isSpecificSearchTerm(term) ? term : undefined;
|
||||
this.fetchGroupLines(this.expandedGroup, useTerm);
|
||||
this.fetchGroupLines(this.expandedGroup, this.getActiveExpandedGroupSearchTerm());
|
||||
}
|
||||
this.loadGroups();
|
||||
this.loadKpis();
|
||||
|
|
@ -4979,9 +4979,7 @@ export class Geral implements OnInit, AfterViewInit, OnDestroy {
|
|||
);
|
||||
|
||||
if (this.expandedGroup) {
|
||||
const term = (this.searchTerm ?? '').trim();
|
||||
const useTerm = term && this.isSpecificSearchTerm(term) ? term : undefined;
|
||||
this.fetchGroupLines(this.expandedGroup, useTerm);
|
||||
this.fetchGroupLines(this.expandedGroup, this.getActiveExpandedGroupSearchTerm());
|
||||
}
|
||||
this.loadGroups();
|
||||
this.loadKpis();
|
||||
|
|
|
|||
|
|
@ -280,6 +280,7 @@ export class ImportPageTemplateService {
|
|||
rows: [],
|
||||
headerRows,
|
||||
excludeItemAndIdColumns: false,
|
||||
skipTemplateStyle: true,
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@ export interface TableExportRequest<T> {
|
|||
headerRows?: Array<Array<string | number | boolean | Date | null | undefined>>;
|
||||
excludeItemAndIdColumns?: boolean;
|
||||
templateBuffer?: ArrayBuffer | null;
|
||||
skipTemplateStyle?: boolean;
|
||||
}
|
||||
|
||||
type ExcelJsModule = typeof import('exceljs');
|
||||
|
|
@ -61,7 +62,9 @@ export class TableExportService {
|
|||
|
||||
async exportAsXlsx<T>(request: TableExportRequest<T>): Promise<void> {
|
||||
const excelJsModule = await this.getExcelJs();
|
||||
const templateBuffer = request.templateBuffer ?? (await this.getDefaultTemplateBuffer());
|
||||
const templateBuffer = request.skipTemplateStyle
|
||||
? null
|
||||
: (request.templateBuffer ?? (await this.getDefaultTemplateBuffer()));
|
||||
const templateStyle = await this.resolveTemplateStyle(excelJsModule, templateBuffer);
|
||||
const workbook = new excelJsModule.Workbook();
|
||||
const sheet = workbook.addWorksheet(this.sanitizeSheetName(request.sheetName || 'Dados'));
|
||||
|
|
|
|||
Loading…
Reference in New Issue