340 lines
15 KiB
HTML
340 lines
15 KiB
HTML
<!-- faturamento.html (COMPLETO - tabela enxuta + modal detalhes completo) -->
|
||
|
||
<!-- Toast (Sucesso) -->
|
||
<div class="toast-container position-fixed top-0 end-0 p-3" style="z-index: 10000;">
|
||
<div #successToast class="toast text-bg-success border-0 shadow" role="alert" aria-live="assertive" aria-atomic="true">
|
||
<div class="toast-header border-bottom-0">
|
||
<strong class="me-auto text-primary">LineGestão</strong>
|
||
<button type="button" class="btn-close" data-bs-dismiss="toast" aria-label="Fechar"></button>
|
||
</div>
|
||
<div class="toast-body bg-white rounded-bottom text-dark">
|
||
{{ toastMessage }}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<section class="fat-page" (click)="closeClientDropdown()">
|
||
<span class="page-blob blob-1" aria-hidden="true"></span>
|
||
<span class="page-blob blob-2" aria-hidden="true"></span>
|
||
<span class="page-blob blob-3" aria-hidden="true"></span>
|
||
<span class="page-blob blob-4" aria-hidden="true"></span>
|
||
|
||
<div class="container-fat">
|
||
<div class="fat-card" data-animate>
|
||
|
||
<div class="fat-header">
|
||
<div class="header-row-top">
|
||
<div class="title-badge" data-animate>
|
||
<i class="bi bi-cash-coin"></i> Faturamento
|
||
</div>
|
||
|
||
<div class="header-title" data-animate>
|
||
<h5 class="title mb-0">Faturamento</h5>
|
||
<small class="subtitle">Totais, lucro e comparativo Vivo x Line</small>
|
||
</div>
|
||
|
||
<div class="header-actions d-flex gap-2 justify-content-end" data-animate>
|
||
<button type="button" class="btn btn-glass btn-sm" (click)="onExport()" [disabled]="loading || exporting">
|
||
<span *ngIf="!exporting"><i class="bi bi-download me-1"></i> Exportar</span>
|
||
<span *ngIf="exporting"><span class="spinner-border spinner-border-sm me-2"></span> Exportando...</span>
|
||
</button>
|
||
<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>
|
||
|
||
<!-- FILTROS -->
|
||
<div class="filters-row mt-4" data-animate>
|
||
<div class="filter-tabs">
|
||
<button type="button" class="filter-tab" [class.active]="filterTipo === 'ALL'" (click)="setFilter('ALL')" [disabled]="loading">
|
||
Todos
|
||
</button>
|
||
|
||
<button type="button" class="filter-tab" [class.active]="filterTipo === 'PF'" (click)="setFilter('PF')" [disabled]="loading">
|
||
<i class="bi bi-person me-1"></i> Pessoa Física
|
||
</button>
|
||
|
||
<button type="button" class="filter-tab" [class.active]="filterTipo === 'PJ'" (click)="setFilter('PJ')" [disabled]="loading">
|
||
<i class="bi bi-building me-1"></i> Pessoa Jurídica
|
||
</button>
|
||
</div>
|
||
|
||
<!-- CLIENTE MULTI-SELECT -->
|
||
<div class="client-filter-wrap" (click)="$event.stopPropagation()">
|
||
<button
|
||
type="button"
|
||
class="btn-client-filter"
|
||
[class.has-selection]="selectedClients.length > 0"
|
||
(click)="toggleClientMenu()"
|
||
[disabled]="loading">
|
||
|
||
<ng-container *ngIf="selectedClients.length === 0">
|
||
<i class="bi bi-people-fill me-2"></i>
|
||
<span>Clientes</span>
|
||
<i class="bi bi-chevron-down ms-2 small"></i>
|
||
</ng-container>
|
||
|
||
<ng-container *ngIf="selectedClients.length > 0">
|
||
<div class="chips-container">
|
||
<span *ngFor="let client of selectedClients" class="client-chip" (click)="$event.stopPropagation()">
|
||
{{ client }}
|
||
<i class="bi bi-x chip-close" (click)="removeClient(client, $event)"></i>
|
||
</span>
|
||
</div>
|
||
<i class="bi bi-chevron-down ms-1 small text-muted"></i>
|
||
</ng-container>
|
||
</button>
|
||
|
||
<div class="client-dropdown" *ngIf="showClientMenu">
|
||
<div class="dropdown-header-search">
|
||
<input
|
||
type="text"
|
||
class="form-control form-control-sm"
|
||
placeholder="Buscar na lista..."
|
||
[(ngModel)]="clientSearchTerm"
|
||
autofocus
|
||
(click)="$event.stopPropagation()">
|
||
</div>
|
||
|
||
<div class="dropdown-list">
|
||
<div class="dropdown-item-custom" [class.selected]="selectedClients.length === 0" (click)="selectClient(null)">
|
||
<i class="bi bi-grid me-2"></i> Todos os Clientes
|
||
</div>
|
||
|
||
<ng-container *ngFor="let client of filteredClientsList">
|
||
<div class="dropdown-item-custom" [class.selected]="isClientSelected(client)" (click)="selectClient(client)">
|
||
<div class="d-flex align-items-center justify-content-between w-100">
|
||
<span>{{ client }}</span>
|
||
<i class="bi bi-check-lg text-brand" *ngIf="isClientSelected(client)"></i>
|
||
</div>
|
||
</div>
|
||
</ng-container>
|
||
</div>
|
||
|
||
<div class="dropdown-footer" *ngIf="selectedClients.length > 0">
|
||
<button class="btn btn-outline-secondary btn-sm w-100" (click)="clearClientSelection($event)">
|
||
<i class="bi bi-x-circle me-1"></i> Limpar seleção
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- KPIs -->
|
||
<div class="fat-kpis mt-4 animate-fade-in">
|
||
<div class="kpi kpi-stack kpi-stack-tight">
|
||
<span class="lbl">Clientes Faturados</span>
|
||
<span class="val">
|
||
<span *ngIf="loadingKpis" class="spinner-border spinner-border-sm text-muted"></span>
|
||
<span *ngIf="!loadingKpis">{{ kpiTotalClientes || 0 }}</span>
|
||
</span>
|
||
</div>
|
||
|
||
<div class="kpi kpi-stack kpi-stack-tight">
|
||
<span class="lbl">Linhas Faturadas</span>
|
||
<span class="val">
|
||
<span *ngIf="loadingKpis" class="spinner-border spinner-border-sm text-muted"></span>
|
||
<span *ngIf="!loadingKpis">{{ kpiTotalLinhas || 0 }}</span>
|
||
</span>
|
||
</div>
|
||
|
||
<div class="kpi kpi-wide kpi-stack">
|
||
<span class="lbl text-vivo">Total Vivo</span>
|
||
<span class="val">
|
||
<span *ngIf="loadingKpis" class="spinner-border spinner-border-sm text-muted"></span>
|
||
<span *ngIf="!loadingKpis">{{ formatMoney(kpiTotalVivo) }}</span>
|
||
</span>
|
||
</div>
|
||
|
||
<div class="kpi kpi-wide kpi-stack">
|
||
<span class="lbl text-line">Total Line</span>
|
||
<span class="val">
|
||
<span *ngIf="loadingKpis" class="spinner-border spinner-border-sm text-muted"></span>
|
||
<span *ngIf="!loadingKpis">{{ formatMoney(kpiTotalLine) }}</span>
|
||
</span>
|
||
</div>
|
||
|
||
<div class="kpi kpi-stack kpi-stack-tight">
|
||
<span class="lbl text-brand">Lucro</span>
|
||
<span class="val">
|
||
<span *ngIf="loadingKpis" class="spinner-border spinner-border-sm text-muted"></span>
|
||
<span *ngIf="!loadingKpis">{{ formatMoney(kpiLucro) }}</span>
|
||
</span>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 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>
|
||
|
||
<div class="page-size d-flex align-items-center gap-2">
|
||
<span class="text-muted small fw-bold text-uppercase" style="letter-spacing: 0.5px; font-size: 0.75rem;">
|
||
Clientes por pág:
|
||
</span>
|
||
|
||
<div class="select-wrapper">
|
||
<app-select class="select-glass" size="sm" [options]="pageSizeOptions" [(ngModel)]="pageSize" (ngModelChange)="onPageSizeChange()" [disabled]="loading"></app-select>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- BODY: GRUPOS -->
|
||
<div class="fat-body">
|
||
<div class="groups-container">
|
||
<div class="text-center p-5" *ngIf="loading">
|
||
<span class="spinner-border text-brand"></span>
|
||
</div>
|
||
|
||
<div class="empty-group" *ngIf="!loading && pagedClientGroups.length === 0">
|
||
Nenhum dado encontrado.
|
||
</div>
|
||
|
||
<div class="group-list" *ngIf="!loading">
|
||
<div
|
||
*ngFor="let g of pagedClientGroups; trackBy: trackByCliente"
|
||
class="client-group-card"
|
||
[class.expanded]="expandedGroup === g.cliente">
|
||
|
||
<div class="group-header" (click)="toggleGroup(g.cliente)">
|
||
<div class="group-info">
|
||
<h6 class="mb-0 fw-bold text-dark td-clip" [title]="g.cliente">{{ g.cliente }}</h6>
|
||
|
||
<div class="group-badges">
|
||
<span class="badge-pill total">{{ g.total }} Registros</span>
|
||
<span class="badge-pill lines">{{ g.linhas }} Linhas</span>
|
||
<span class="badge-pill vivo">{{ formatMoney(g.totalVivo) }}</span>
|
||
<span class="badge-pill line">{{ formatMoney(g.totalLine) }}</span>
|
||
<span class="badge-pill lucro" *ngIf="g.lucro !== 0">{{ formatMoney(g.lucro) }}</span>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="group-toggle-icon"><i class="bi bi-chevron-down"></i></div>
|
||
</div>
|
||
|
||
<div class="group-body" *ngIf="expandedGroup === g.cliente">
|
||
<div class="group-actions-row">
|
||
<small class="text-muted fw-bold">Registros do Cliente</small>
|
||
<span class="chip-muted"><i class="bi bi-info-circle me-1"></i> Clique no “olho” para ver todos os detalhes</span>
|
||
</div>
|
||
|
||
<div class="table-wrap inner-table-wrap">
|
||
<table class="table table-modern table-compact align-middle text-center mb-0">
|
||
<thead>
|
||
<tr class="thead-group">
|
||
<th rowspan="2" class="sortable th-item" (click)="setSort('item')">
|
||
<div class="th-content">ITEM</div>
|
||
</th>
|
||
|
||
<th rowspan="2" class="sortable" (click)="setSort('qtdlinhas')">
|
||
<div class="th-content">QTD LINHAS</div>
|
||
</th>
|
||
|
||
<th colspan="2" class="th-block th-vivo">VIVO</th>
|
||
<th colspan="2" class="th-block th-line">LINE MÓVEL</th>
|
||
|
||
<th rowspan="2" class="actions-col">AÇÕES</th>
|
||
</tr>
|
||
|
||
<tr class="thead-sub">
|
||
<th class="sortable" (click)="setSort('franquiavivo')">
|
||
<div class="th-content">FRANQUIA</div>
|
||
</th>
|
||
|
||
<th class="sortable" (click)="setSort('valorcontratovivo')">
|
||
<div class="th-content">VALOR (R$)</div>
|
||
</th>
|
||
|
||
<th class="sortable" (click)="setSort('franquialine')">
|
||
<div class="th-content">FRANQUIA</div>
|
||
</th>
|
||
|
||
<th class="sortable" (click)="setSort('valorcontratoline')">
|
||
<div class="th-content">VALOR (R$)</div>
|
||
</th>
|
||
</tr>
|
||
</thead>
|
||
|
||
<tbody>
|
||
<tr *ngIf="groupRows.length === 0">
|
||
<td colspan="7" class="text-center py-4 empty-state text-muted fw-bold">Nenhum registro.</td>
|
||
</tr>
|
||
|
||
<tr *ngFor="let r of groupRows; trackBy: trackById" class="table-row-item">
|
||
<td class="text-muted fw-bold">{{ r.item }}</td>
|
||
|
||
<td class="fw-black">{{ r.qtdLinhas ?? 0 }}</td>
|
||
|
||
<td class="fw-bold">{{ formatFranquia(r.franquiaVivo) }}</td>
|
||
<td class="fw-bold text-vivo">{{ formatMoney(r.valorContratoVivo) }}</td>
|
||
|
||
<td class="fw-bold">{{ formatFranquia(r.franquiaLine) }}</td>
|
||
<td class="fw-bold text-line">{{ formatMoney(r.valorContratoLine) }}</td>
|
||
|
||
<td>
|
||
<div class="action-group justify-content-center">
|
||
<button class="btn-icon" (click)="onDetalhes(r)" title="Detalhes"><i class="bi bi-eye"></i></button>
|
||
<button class="btn-icon success" (click)="onComparativo(r)" title="Comparativo Vivo x Line"><i class="bi bi-columns-gap"></i></button>
|
||
<button *ngIf="isSysAdmin" class="btn-icon primary" (click)="onEditar(r)" title="Editar"><i class="bi bi-pencil-square"></i></button>
|
||
<button *ngIf="isSysAdmin" class="btn-icon danger" (click)="onDelete(r)" title="Excluir"><i class="bi bi-trash"></i></button>
|
||
</div>
|
||
</td>
|
||
</tr>
|
||
</tbody>
|
||
|
||
</table>
|
||
</div>
|
||
</div>
|
||
|
||
</div>
|
||
</div>
|
||
|
||
</div>
|
||
</div>
|
||
|
||
<div class="fat-footer">
|
||
<div class="small text-muted fw-bold">Mostrando {{ pageStart }}–{{ pageEnd }} de {{ total }}</div>
|
||
|
||
<nav>
|
||
<ul class="pagination pagination-sm mb-0 pagination-modern">
|
||
<li class="page-item" [class.disabled]="page === 1 || loading">
|
||
<button class="page-link" (click)="goToPage(page - 1)">Anterior</button>
|
||
</li>
|
||
|
||
<li class="page-item" *ngFor="let p of pageNumbers" [class.active]="p === page">
|
||
<button class="page-link" (click)="goToPage(p)">{{ p }}</button>
|
||
</li>
|
||
|
||
<li class="page-item" [class.disabled]="page === totalPages || loading">
|
||
<button class="page-link" (click)="goToPage(page + 1)">Próxima</button>
|
||
</li>
|
||
</ul>
|
||
</nav>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
|
||
<!-- MODAIS -->
|
||
|
||
<app-faturamento-modals [vm]="$any(vm)"></app-faturamento-modals>
|