This commit is contained in:
Luiz Silva 2026-02-12 16:38:17 -03:00
parent 63d943d0df
commit f396203085
22 changed files with 1476 additions and 1357 deletions

View file

@ -1,6 +1,12 @@
<template>
<div class="eli-tabela">
<EliTabelaDebug :isDev="isDev" :menuAberto="menuAberto" :menuPopupPos="menuPopupPos" />
<EliTabelaDebug :isDev="isDev" :menuAberto="menuAberto" :menuPopupPos="menuPopupPos">
<div>paginaAtual: {{ paginaAtual }}</div>
<div>limit: {{ registrosPorConsulta }}</div>
<div>texto_busca: {{ (valorBusca || '').trim() }}</div>
<div>filtrosAvancadosAtivos: {{ JSON.stringify(filtrosAvancadosAtivos) }}</div>
<div>quantidadeTotal: {{ quantidade }}</div>
</EliTabelaDebug>
<EliTabelaEstados
v-if="carregando || Boolean(erro) || !linhas.length"
@ -193,14 +199,22 @@ export default defineComponent({
filtrosUi.value = [];
limparFiltroAvancado(props.tabela.nome);
modalFiltroAberto.value = false;
// Se o usuário estiver usando filtro avançado, a busca deixa de ter efeito.
// Mantemos a regra combinatória (busca tem prioridade), então limpamos a busca.
valorBusca.value = "";
if (paginaAtual.value !== 1) paginaAtual.value = 1;
else void carregar();
}
function salvarFiltrosAvancados(novo: any[]) {
filtrosUi.value = (novo ?? []) as any;
salvarFiltroAvancado(props.tabela.nome, (novo ?? []) as any);
modalFiltroAberto.value = false;
// Ao aplicar filtros, limpamos a busca para garantir que os filtros sejam efetivos.
// (busca tem prioridade sobre filtros)
valorBusca.value = "";
if (paginaAtual.value !== 1) paginaAtual.value = 1;
else void carregar();
}
const filtrosAvancadosAtivos = computed<tipoFiltro[]>(() => {
@ -351,85 +365,20 @@ export default defineComponent({
return 10;
});
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 */
/** Total de páginas calculado com base no total retornado pela API */
const totalPaginas = computed(() => {
const limite = registrosPorConsulta.value;
if (!limite || limite <= 0) return 1;
const total = quantidadeFiltrada.value;
const total = quantidade.value ?? 0;
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);
});
/** As linhas já vêm paginadas do backend */
const linhasPaginadas = computed(() => linhas.value ?? []);
/** Quantidade exibida é a quantidade total retornada pela consulta */
const quantidadeFiltrada = computed(() => quantidade.value ?? 0);
/** Indica se existem ações por linha */
const temAcoes = computed(() => (props.tabela.acoesLinha ?? []).length > 0);
@ -594,12 +543,11 @@ 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 = 0;
const offset = (paginaAtual.value - 1) * limite;
const parametrosConsulta: {
filtros?: tipoFiltro[];
coluna_ordem?: never;
direcao_ordem?: "asc" | "desc";
offSet: number;
@ -607,16 +555,28 @@ export default defineComponent({
texto_busca?: string;
} = {
offSet: offset,
limit: 999999,
limit: limite,
};
// texto_busca ficará somente para filtragem local.
// Regra combinatória definida: busca tem prioridade.
const busca = (valorBusca.value ?? "").trim();
if (busca) {
parametrosConsulta.texto_busca = busca;
} else {
const filtros = filtrosAvancadosAtivos.value;
if (filtros.length) parametrosConsulta.filtros = filtros;
}
if (colunaOrdenacao.value) {
parametrosConsulta.coluna_ordem = colunaOrdenacao.value as never;
parametrosConsulta.direcao_ordem = direcaoOrdenacao.value;
}
if (import.meta.env.DEV) {
// eslint-disable-next-line no-console
console.log("[EliTabela] consulta(parametros)", parametrosConsulta);
}
try {
const tabelaConfig = props.tabela;
const res = await tabelaConfig.consulta(parametrosConsulta);
@ -631,16 +591,13 @@ export default defineComponent({
}
const valores = res.valor?.valores ?? [];
const total = valores.length;
const total = (res.valor as any)?.quantidade ?? valores.length;
linhas.value = valores;
quantidade.value = total;
quantidade.value = Number(total) || 0;
const totalPaginasRecalculado = Math.max(1, Math.ceil((quantidadeFiltrada.value || 0) / limite));
if (paginaAtual.value > totalPaginasRecalculado) {
paginaAtual.value = totalPaginasRecalculado;
return;
}
const totalPaginasRecalculado = Math.max(1, Math.ceil((quantidade.value || 0) / limite));
if (paginaAtual.value > totalPaginasRecalculado) paginaAtual.value = totalPaginasRecalculado;
const acoesLinhaConfiguradas = tabelaConfig.acoesLinha ?? [];
if (!acoesLinhaConfiguradas.length) {
@ -720,10 +677,7 @@ export default defineComponent({
/** Watch: mudança de página dispara nova consulta */
watch(paginaAtual, (nova, antiga) => {
// paginação local não precisa recarregar
if (nova !== antiga) {
// noop
}
if (nova !== antiga) void carregar();
});
/** Watch: troca de configuração reseta estados e recarrega */
@ -774,6 +728,7 @@ export default defineComponent({
erro,
linhas,
linhasPaginadas,
filtrosAvancadosAtivos,
quantidadeFiltrada,
quantidade,
menuAberto,
@ -782,6 +737,7 @@ export default defineComponent({
colunaOrdenacao,
direcaoOrdenacao,
totalPaginas,
registrosPorConsulta,
// computed
exibirBusca,

View file

@ -7,6 +7,7 @@
<div><b>EliTabela debug</b></div>
<div>menuAberto: {{ menuAberto }}</div>
<div>menuPos: top={{ menuPopupPos.top }}, left={{ menuPopupPos.left }}</div>
<slot />
</div>
</template>

View file

@ -37,7 +37,11 @@
</div>
<div class="eli-tabela-modal-filtro__acoes">
<select v-model="colunaParaAdicionar" class="eli-tabela-modal-filtro__select" :disabled="!opcoesParaAdicionar.length">
<select
v-model="colunaParaAdicionar"
class="eli-tabela-modal-filtro__select"
:disabled="!opcoesParaAdicionar.length"
>
<option disabled value="">Selecione um filtro</option>
<option v-for="o in opcoesParaAdicionar" :key="String(o.coluna)" :value="String(o.coluna)">
{{ rotuloDoFiltro(o) }}

View file

@ -1,12 +1,11 @@
<template>
<!-- TODO: Validar de ação está cehgando aqui-->
<button
v-if="dados?.acao"
type="button"
class="eli-tabela__texto-truncado eli-tabela__celula-link"
:title="dados?.texto"
@click.stop.prevent="dados.acao()"
@click.stop.prevent="dados?.acao?.()"
>
{{ dados?.texto }}
</button>

View file

@ -96,7 +96,6 @@ export type EliTabelaConsulta<T> = {
*/
consulta: (parametrosConsulta?: {
//Todo: Esse filtros são recebido do processamento de filtro avandado
filtros?: tipoFiltro[]
coluna_ordem?: keyof T;
@ -125,12 +124,19 @@ export type EliTabelaConsulta<T> = {
rotulo: string;
/** Função executada ao clicar no botão. */
acao: () => void;
/**
* Callback opcional para forçar atualização da consulta.
* Observação: o componente `EliTabela` pode ignorar isso dependendo do modo de uso.
*/
atualizarConsulta?: () => Promise<void>
/**
* Callback opcional para permitir editar a lista localmente (sem refazer consulta).
* Observação: o componente `EliTabela` pode ignorar isso dependendo do modo de uso.
*/
editarLista?: (lista: T[]) => Promise<T[]>
}[];
/** configuração para aplicação dos filtros padrões */
// Todo: quando exite aparace ap lado do obtão coluna o potão filtro avançado, onde abre um modal com dua colunas de compoentes que são contruidas conforme esse padrão
// todo: Os filtros criados deverão ser salvo em local storagem como um objeto tipofiltro[]
filtroAvancado?: {
rotulo: string,
coluna: keyof T,

View file

@ -17,8 +17,13 @@ export { EliOlaMundo };
export { EliBotao };
export { EliBadge };
export { EliCartao };
export { EliTabela };
export { EliEntradaTexto, EliEntradaNumero, EliEntradaDataHora, EliEntradaParagrafo, EliEntradaSelecao };
// Exportar tudo (componentes + types + helpers) de Tabela e Entrada
export * from "./componentes/EliTabela";
export * from "./componentes/EliEntrada";
// Exportar tipos compartilhados (ex: CartaoStatus)
export * from "./tipos";
const EliVue: Plugin = {
install(app: App) {

View file

@ -18,6 +18,7 @@ import { BadgeCheck, Eye, Pencil, Plus, Trash2 } from "lucide-vue-next";
import { celulaTabela, EliTabela } from "@/componentes/EliTabela";
import type { ComponenteEntrada } from "@/componentes/EliEntrada/tiposEntradas";
import type { EliTabelaConsulta } from "@/componentes/EliTabela";
import type { tipoFiltro } from "@/componentes/EliTabela/types-eli-tabela";
type Linha = {
empreendedor: string;
@ -324,21 +325,61 @@ export default defineComponent({
});
};
const ordenarLinhas = (
linhas: Linha[],
parametros?: { coluna_ordem?: keyof Linha; direcao_ordem?: "asc" | "desc"; texto_busca?: string }
) => {
const listaFiltrada = filtrarPorBusca(linhas, parametros?.texto_busca);
if (!parametros?.coluna_ordem) {
return listaFiltrada;
const 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": {
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;
}
};
const filtrarPorFiltrosAvancados = (linhas: Linha[], filtros?: tipoFiltro[]) => {
const lista = [...linhas];
if (!filtros?.length) return lista;
return lista.filter((linha: any) => {
return filtros.every((f) => {
const vLinha = linha?.[String((f as any).coluna)];
return compararOperador(String((f as any).operador), vLinha, (f as any).valor);
});
});
};
const ordenarLinhas = (linhas: Linha[], parametros?: { coluna_ordem?: keyof Linha; direcao_ordem?: "asc" | "desc" }) => {
if (!parametros?.coluna_ordem) return [...linhas];
const direcao = parametros.direcao_ordem ?? "asc";
const chave = parametros.coluna_ordem;
const multiplicador = direcao === "asc" ? 1 : -1;
return [...listaFiltrada].sort((a, b) => {
return [...linhas].sort((a, b) => {
const valorA = a[chave];
const valorB = b[chave];
@ -483,10 +524,23 @@ export default defineComponent({
},
],
consulta: async (parametrosConsulta) => {
// No filtro avançado (modo simulação), a EliTabela busca a lista completa
// e pagina/filtra localmente.
const ordenadas = ordenarLinhas(linhasPadrao.value, parametrosConsulta);
const valores = ordenadas;
// Agora a EliTabela envia paginação/ordenação/busca OU filtros avançados para a consulta.
// (busca tem prioridade; quando existe texto_busca, filtros não vêm no payload)
const limite = Math.max(1, Number(parametrosConsulta?.limit ?? 10));
const offset = Math.max(0, Number(parametrosConsulta?.offSet ?? 0));
// 1) filtra (busca OU filtro avançado)
const base = [...linhasPadrao.value];
const filtradas = parametrosConsulta?.texto_busca
? filtrarPorBusca(base, parametrosConsulta.texto_busca)
: filtrarPorFiltrosAvancados(base, (parametrosConsulta as any)?.filtros);
// 2) ordena
const ordenadas = ordenarLinhas(filtradas, parametrosConsulta);
// 3) pagina
const valores = ordenadas.slice(offset, offset + limite);
return {
cod: codigosResposta.sucesso,