This commit is contained in:
Luiz Silva 2026-01-29 13:38:24 -03:00
parent 0144788548
commit e7357e064a
19 changed files with 14478 additions and 1364 deletions

View file

@ -13,10 +13,12 @@
<EliTabelaCabecalho
v-if="exibirBusca || temAcoesCabecalho"
:exibirBusca="exibirBusca"
:exibirBotaoFiltroAvancado="exibirFiltroAvancado"
:valorBusca="valorBusca"
:acoesCabecalho="acoesCabecalho"
@buscar="atualizarBusca"
@colunas="abrirModalColunas"
@filtroAvancado="abrirModalFiltro"
/>
<EliTabelaModalColunas
@ -28,6 +30,15 @@
@salvar="salvarModalColunas"
/>
<EliTabelaModalFiltroAvancado
:aberto="modalFiltroAberto"
:filtrosBase="tabela.filtroAvancado ?? []"
:modelo="filtrosUi"
@fechar="fecharModalFiltro"
@limpar="limparFiltrosAvancados"
@salvar="salvarFiltrosAvancados"
/>
<table class="eli-tabela__table">
<EliTabelaHead
:colunas="colunasEfetivas"
@ -43,7 +54,7 @@
:colunasInvisiveis="colunasInvisiveisEfetivas"
:temColunasInvisiveis="temColunasInvisiveis"
:linhasExpandidas="linhasExpandidas"
:linhas="linhas"
:linhas="linhasPaginadas"
:temAcoes="temAcoes"
:menuAberto="menuAberto"
:possuiAcoes="possuiAcoes"
@ -57,12 +68,12 @@
:menuAberto="menuAberto"
:posicao="menuPopupPos"
:acoes="menuAberto === null ? [] : acoesDisponiveisPorLinha(menuAberto)"
:linha="menuAberto === null ? null : linhas[menuAberto]"
:linha="menuAberto === null ? null : linhasPaginadas[menuAberto]"
@executar="({ acao, linha }) => { menuAberto = null; acao.acao(linha as never); }"
/>
<EliTabelaPaginacao
v-if="totalPaginas > 1 && quantidade > 0"
v-if="totalPaginas > 1 && quantidadeFiltrada > 0"
:pagina="paginaAtual"
:totalPaginas="totalPaginas"
:maximoBotoes="tabela.maximo_botoes_paginacao"
@ -99,15 +110,25 @@ import EliTabelaBody from "./EliTabelaBody.vue";
import EliTabelaMenuAcoes from "./EliTabelaMenuAcoes.vue";
import EliTabelaPaginacao from "./EliTabelaPaginacao.vue";
import EliTabelaModalColunas from "./EliTabelaModalColunas.vue";
import EliTabelaModalFiltroAvancado from "./EliTabelaModalFiltroAvancado.vue";
import type { EliColuna } from "./types-eli-tabela";
/** Tipos da configuração/contrato da tabela */
import type { EliTabelaConsulta } from "./types-eli-tabela";
import type { tipoFiltro } from "./types-eli-tabela";
import type { ComponenteEntrada } from "../EliEntrada/tiposEntradas";
import { operadores as Operadores } from "p-comuns";
import {
carregarConfigColunas,
salvarConfigColunas,
type EliTabelaColunasConfig,
} from "./colunasStorage";
import {
carregarFiltroAvancado,
salvarFiltroAvancado,
limparFiltroAvancado,
} from "./filtroAvancadoStorage";
export default defineComponent({
name: "EliTabela",
inheritAttrs: false,
@ -120,6 +141,7 @@ export default defineComponent({
EliTabelaMenuAcoes,
EliTabelaPaginacao,
EliTabelaModalColunas,
EliTabelaModalFiltroAvancado,
},
props: {
/** Configuração principal da tabela (colunas, consulta e ações) */
@ -151,6 +173,53 @@ export default defineComponent({
const colunaOrdenacao = ref<string | null>(null);
const direcaoOrdenacao = ref<"asc" | "desc">("asc");
/** Filtro avançado (config + estado modal) */
const modalFiltroAberto = ref(false);
type LinhaFiltroUI<T> = {
coluna: keyof T;
operador: keyof typeof Operadores;
entrada: ComponenteEntrada;
valor: any;
};
const filtrosUi = ref<Array<LinhaFiltroUI<any>>>(
carregarFiltroAvancado<any>(props.tabela.nome) as any
);
function abrirModalFiltro() {
modalFiltroAberto.value = true;
}
function fecharModalFiltro() {
modalFiltroAberto.value = false;
}
function limparFiltrosAvancados() {
filtrosUi.value = [];
limparFiltroAvancado(props.tabela.nome);
modalFiltroAberto.value = false;
if (paginaAtual.value !== 1) paginaAtual.value = 1;
}
function salvarFiltrosAvancados(novo: any[]) {
filtrosUi.value = (novo ?? []) as any;
salvarFiltroAvancado(props.tabela.nome, (novo ?? []) as any);
modalFiltroAberto.value = false;
if (paginaAtual.value !== 1) paginaAtual.value = 1;
}
const filtrosAvancadosAtivos = computed<tipoFiltro[]>(() => {
// converte UI -> tipoFiltro (p-comuns)
return (filtrosUi.value ?? [])
.filter((f) => f && f.coluna && f.operador)
.map((f) => ({
coluna: String(f.coluna),
operador: f.operador as any,
valor: f.valor,
// sem OR no primeiro momento
})) as tipoFiltro[];
});
/** Alias reativo da prop tabela */
const tabela = computed(() => props.tabela);
@ -280,20 +349,91 @@ export default defineComponent({
return 10;
});
/** Total de páginas calculado com base na quantidade */
function aplicarFiltroTexto(linhasIn: unknown[]) {
const q = (valorBusca.value ?? "").trim().toLowerCase();
if (!q) return linhasIn;
// filtro simples: stringifica o objeto
return linhasIn.filter((l) => JSON.stringify(l).toLowerCase().includes(q));
}
function compararOperador(operador: string, valorLinha: any, valorFiltro: any): boolean {
switch (operador) {
case "=":
return valorLinha == valorFiltro;
case "!=":
return valorLinha != valorFiltro;
case ">":
return Number(valorLinha) > Number(valorFiltro);
case ">=":
return Number(valorLinha) >= Number(valorFiltro);
case "<":
return Number(valorLinha) < Number(valorFiltro);
case "<=":
return Number(valorLinha) <= Number(valorFiltro);
case "like": {
const a = String(valorLinha ?? "").toLowerCase();
const b = String(valorFiltro ?? "").toLowerCase();
return a.includes(b);
}
case "in": {
// aceita "a,b,c" ou array
const arr = Array.isArray(valorFiltro)
? valorFiltro
: String(valorFiltro ?? "")
.split(",")
.map((s) => s.trim())
.filter(Boolean);
return arr.includes(String(valorLinha));
}
case "isNull":
return valorLinha === null || valorLinha === undefined || valorLinha === "";
default:
return true;
}
}
function aplicarFiltroAvancado(linhasIn: unknown[]) {
const filtros = filtrosAvancadosAtivos.value;
if (!filtros.length) return linhasIn;
return linhasIn.filter((l: any) => {
return filtros.every((f) => {
const vLinha = l?.[f.coluna as any];
return compararOperador(String(f.operador), vLinha, (f as any).valor);
});
});
}
const linhasFiltradas = computed(() => {
const base = linhas.value ?? [];
return aplicarFiltroAvancado(aplicarFiltroTexto(base));
});
/** Quantidade agora segue a filtragem local */
const quantidadeFiltrada = computed(() => linhasFiltradas.value.length);
/** Total de páginas calculado com base no filtrado */
const totalPaginas = computed(() => {
const limite = registrosPorConsulta.value;
if (!limite || limite <= 0) return 1;
const total = quantidade.value;
const total = quantidadeFiltrada.value;
if (!total) return 1;
return Math.max(1, Math.ceil(total / limite));
});
const linhasPaginadas = computed(() => {
const limite = Math.max(1, registrosPorConsulta.value);
const offset = (paginaAtual.value - 1) * limite;
return linhasFiltradas.value.slice(offset, offset + limite);
});
/** Indica se existem ações por linha */
const temAcoes = computed(() => (props.tabela.acoesLinha ?? []).length > 0);
const exibirFiltroAvancado = computed(() => (props.tabela.filtroAvancado ?? []).length > 0);
/** Sequencial para evitar race conditions entre consultas */
let carregamentoSequencial = 0;
@ -452,8 +592,10 @@ export default defineComponent({
menuAberto.value = null;
linhasExpandidas.value = {};
// Em modo simulação (filtro local), sempre buscamos a lista completa.
// A paginação é aplicada APÓS a filtragem.
const limite = Math.max(1, registrosPorConsulta.value);
const offset = (paginaAtual.value - 1) * limite;
const offset = 0;
const parametrosConsulta: {
coluna_ordem?: never;
@ -463,12 +605,10 @@ export default defineComponent({
texto_busca?: string;
} = {
offSet: offset,
limit: limite,
limit: 999999,
};
if (valorBusca.value) {
parametrosConsulta.texto_busca = valorBusca.value;
}
// texto_busca ficará somente para filtragem local.
if (colunaOrdenacao.value) {
parametrosConsulta.coluna_ordem = colunaOrdenacao.value as never;
@ -489,12 +629,12 @@ export default defineComponent({
}
const valores = res.valor?.valores ?? [];
const total = res.valor?.quantidade ?? valores.length;
const total = valores.length;
linhas.value = valores;
quantidade.value = total;
const totalPaginasRecalculado = Math.max(1, Math.ceil((total || 0) / limite));
const totalPaginasRecalculado = Math.max(1, Math.ceil((quantidadeFiltrada.value || 0) / limite));
if (paginaAtual.value > totalPaginasRecalculado) {
paginaAtual.value = totalPaginasRecalculado;
return;
@ -578,7 +718,10 @@ export default defineComponent({
/** Watch: mudança de página dispara nova consulta */
watch(paginaAtual, (nova, antiga) => {
if (nova !== antiga) void carregar();
// paginação local não precisa recarregar
if (nova !== antiga) {
// noop
}
});
/** Watch: troca de configuração reseta estados e recarrega */
@ -590,7 +733,9 @@ export default defineComponent({
direcaoOrdenacao.value = "asc";
valorBusca.value = "";
modalColunasAberto.value = false;
modalFiltroAberto.value = false;
configColunas.value = carregarConfigColunas(props.tabela.nome);
filtrosUi.value = carregarFiltroAvancado<any>(props.tabela.nome) as any;
linhasExpandidas.value = {};
if (paginaAtual.value !== 1) {
paginaAtual.value = 1;
@ -626,6 +771,8 @@ export default defineComponent({
carregando,
erro,
linhas,
linhasPaginadas,
quantidadeFiltrada,
quantidade,
menuAberto,
valorBusca,
@ -636,6 +783,7 @@ export default defineComponent({
// computed
exibirBusca,
exibirFiltroAvancado,
acoesCabecalho,
temAcoesCabecalho,
temAcoes,
@ -649,9 +797,16 @@ export default defineComponent({
linhasExpandidas,
abrirModalColunas,
abrirModalFiltro,
fecharModalColunas,
salvarModalColunas,
modalFiltroAberto,
filtrosUi,
salvarFiltrosAvancados,
limparFiltrosAvancados,
fecharModalFiltro,
alternarLinhaExpandida,
// actions