line-gestao-frontend/src/app/pages/faturamento/faturamento.html

532 lines
22 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!-- 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></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">Total Clientes</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">Total Linhas</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 por cliente, aparelho, forma de pagamento..."
[(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">
<select class="form-select form-select-sm select-glass" [(ngModel)]="pageSize" (change)="onPageSizeChange()" [disabled]="loading">
<option [ngValue]="10">10</option>
<option [ngValue]="20">20</option>
<option [ngValue]="50">50</option>
<option [ngValue]="100">100</option>
</select>
<i class="bi bi-chevron-down select-icon"></i>
</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">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>
</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 -->
<div class="modal-backdrop-custom" *ngIf="detailOpen || compareOpen" (click)="closeAllModals()"></div>
<div class="modal-custom" *ngIf="detailOpen || compareOpen" (click)="closeAllModals()">
<!-- DETAIL MODAL -->
<div *ngIf="detailOpen" #detailModal class="modal-card modal-xl-custom" (click)="$event.stopPropagation()">
<div class="modal-header">
<div class="modal-title">
<span class="icon-bg primary-soft"><i class="bi bi-receipt"></i></span>
Detalhes do Faturamento
</div>
<button class="btn btn-sm btn-icon" (click)="closeAllModals()"><i class="bi bi-x-lg"></i></button>
</div>
<div class="modal-body modern-body bg-light-gray" *ngIf="detailData; else detailLoading">
<div class="mb-3 d-flex flex-wrap align-items-center justify-content-between gap-2">
<div class="d-flex flex-column">
<div class="fw-black" style="font-size: 1.05rem;">
{{ detailData.cliente || '—' }}
</div>
<small class="text-muted fw-bold">
ITEM: {{ detailData.item }} • QTD LINHAS: {{ detailData.qtdLinhas ?? 0 }}
</small>
</div>
<div class="d-flex flex-wrap gap-2">
<span class="badge-pill vivo"><i class="bi bi-telephone-fill me-1"></i> {{ formatMoney(detailData.valorContratoVivo) }}</span>
<span class="badge-pill line"><i class="bi bi-hdd-network-fill me-1"></i> {{ formatMoney(detailData.valorContratoLine) }}</span>
<span class="badge-pill lucro" *ngIf="hasLucro(detailData)">
<i class="bi bi-cash-stack me-1"></i> {{ formatMoney(detailData.lucro) }}
</span>
</div>
</div>
<div class="details-dashboard details-2col">
<!-- IDENTIFICAÇÃO -->
<div class="detail-box">
<div class="box-header justify-content-center">
<span><i class="bi bi-person-badge me-2"></i> Identificação</span>
</div>
<div class="box-body">
<div class="info-grid">
<div class="info-item span-2">
<span class="lbl">Cliente</span>
<span class="val text-dark" [title]="detailData.cliente || ''">{{ detailData.cliente || '—' }}</span>
</div>
<div class="info-item">
<span class="lbl">Tipo</span>
<span class="val">{{ detailData.tipo || '—' }}</span>
</div>
<div class="info-item">
<span class="lbl">Qtd Linhas</span>
<span class="val">{{ detailData.qtdLinhas ?? 0 }}</span>
</div>
<div class="info-item span-2">
<span class="lbl">Aparelho</span>
<span class="val" [title]="detailData.aparelho || ''">{{ detailData.aparelho || '—' }}</span>
</div>
<div class="info-item span-2">
<span class="lbl">Forma de Pagamento</span>
<span class="val" [title]="detailData.formaPagamento || ''">{{ detailData.formaPagamento || '—' }}</span>
</div>
</div>
</div>
</div>
<!-- VIVO -->
<div class="detail-box">
<div class="box-header justify-content-center">
<span><i class="bi bi-telephone-fill me-2"></i> Vivo</span>
</div>
<div class="box-body">
<div class="info-grid">
<div class="info-item">
<span class="lbl">Franquia Vivo</span>
<span class="val fw-black">{{ formatFranquia(detailData.franquiaVivo) }}</span>
</div>
<div class="info-item">
<span class="lbl text-vivo">Valor Vivo (R$)</span>
<span class="val fw-black text-vivo">{{ formatMoney(detailData.valorContratoVivo) }}</span>
</div>
</div>
</div>
</div>
<!-- LINE -->
<div class="detail-box">
<div class="box-header justify-content-center">
<span><i class="bi bi-hdd-network-fill me-2"></i> Line Móvel</span>
</div>
<div class="box-body">
<div class="info-grid">
<div class="info-item">
<span class="lbl">Franquia Line</span>
<span class="val fw-black">{{ formatFranquia(detailData.franquiaLine) }}</span>
</div>
<div class="info-item">
<span class="lbl text-line">Valor Line (R$)</span>
<span class="val fw-black text-line">{{ formatMoney(detailData.valorContratoLine) }}</span>
</div>
<div class="info-item span-2">
<span class="lbl text-brand">Lucro</span>
<span class="val fw-black text-brand">{{ formatMoney(detailData.lucro) }}</span>
</div>
</div>
</div>
</div>
<!-- RESUMO -->
<div class="detail-box">
<div class="box-header justify-content-center">
<span><i class="bi bi-info-circle me-2"></i> Resumo</span>
</div>
<div class="box-body">
<div class="info-grid">
<div class="info-item span-2">
<span class="lbl">Observação</span>
<span class="val">{{ getObservacao(detailData) }}</span>
</div>
</div>
<div class="mt-3 d-flex justify-content-end gap-2 flex-wrap">
<button class="btn btn-outline-secondary btn-sm" (click)="closeAllModals()">
Fechar
</button>
<button class="btn btn-primary btn-sm" (click)="onComparativo(detailData)">
<i class="bi bi-columns-gap me-1"></i> Abrir Comparativo
</button>
</div>
</div>
</div>
</div>
</div>
<ng-template #detailLoading>
<div class="p-5 text-center text-muted">Carregando detalhes...</div>
</ng-template>
</div>
<!-- COMPARATIVO MODAL -->
<div *ngIf="compareOpen" #compareModal class="modal-card modal-lg" (click)="$event.stopPropagation()">
<div class="modal-header">
<div class="modal-title">
<span class="icon-bg success"><i class="bi bi-columns-gap"></i></span> Comparativo Vivo x Line
</div>
<button class="btn btn-sm btn-icon" (click)="closeAllModals()"><i class="bi bi-x-lg"></i></button>
</div>
<div class="modal-body modern-body bg-light-gray" *ngIf="compareData; else compareLoading">
<div class="finance-dashboard">
<div class="finance-card vivo-card">
<div class="card-header-f"><i class="bi bi-telephone-fill me-2"></i> Vivo</div>
<div class="card-body-f">
<div class="row-item"><span>Franquia</span> <strong>{{ formatFranquia(compareData.franquiaVivo) }}</strong></div>
<div class="divider"></div>
<div class="row-item total"><span>Valor Vivo (R$)</span> <strong>{{ formatMoney(compareData.valorContratoVivo) }}</strong></div>
</div>
</div>
<div class="finance-card line-card">
<div class="card-header-f"><i class="bi bi-hdd-network-fill me-2"></i> Line Móvel</div>
<div class="card-body-f">
<div class="row-item"><span>Franquia Line</span> <strong>{{ formatFranquia(compareData.franquiaLine) }}</strong></div>
<div class="divider"></div>
<div class="row-item total"><span>Valor Line (R$)</span> <strong>{{ formatMoney(compareData.valorContratoLine) }}</strong></div>
</div>
</div>
</div>
<div class="finance-summary mt-3">
<div class="summary-item">
<span class="lbl">Forma de Pagamento</span>
<span class="val text-dark">{{ compareData.formaPagamento || '—' }}</span>
</div>
<div class="vertical-line"></div>
<div class="summary-item">
<span class="lbl">Lucro</span>
<span class="val text-brand">{{ formatMoney(compareData.lucro) }}</span>
</div>
</div>
</div>
<ng-template #compareLoading>
<div class="p-5 text-center text-muted">Carregando comparativo...</div>
</ng-template>
</div>
</div>