828 lines
25 KiB
Vue
828 lines
25 KiB
Vue
<template>
|
|
<div class="eli-tabela">
|
|
<EliTabelaDebug :isDev="isDev" :menuAberto="menuAberto" :menuPopupPos="menuPopupPos" />
|
|
|
|
<EliTabelaEstados
|
|
v-if="carregando || Boolean(erro) || !linhas.length"
|
|
:carregando="carregando"
|
|
:erro="erro"
|
|
:mensagemVazio="tabela.mensagemVazio"
|
|
/>
|
|
|
|
<template v-else>
|
|
<EliTabelaCabecalho
|
|
v-if="exibirBusca || temAcoesCabecalho"
|
|
:exibirBusca="exibirBusca"
|
|
:exibirBotaoFiltroAvancado="exibirFiltroAvancado"
|
|
:valorBusca="valorBusca"
|
|
:acoesCabecalho="acoesCabecalho"
|
|
@buscar="atualizarBusca"
|
|
@colunas="abrirModalColunas"
|
|
@filtroAvancado="abrirModalFiltro"
|
|
/>
|
|
|
|
<EliTabelaModalColunas
|
|
:aberto="modalColunasAberto"
|
|
:rotulosColunas="rotulosColunas"
|
|
:configInicial="configColunas"
|
|
:colunas="tabela.colunas"
|
|
@fechar="fecharModalColunas"
|
|
@salvar="salvarModalColunas"
|
|
/>
|
|
|
|
<EliTabelaModalFiltroAvancado
|
|
:aberto="modalFiltroAberto"
|
|
:filtrosBase="tabela.filtroAvancado ?? []"
|
|
:modelo="filtrosUi"
|
|
@fechar="fecharModalFiltro"
|
|
@limpar="limparFiltrosAvancados"
|
|
@salvar="salvarFiltrosAvancados"
|
|
/>
|
|
|
|
<table class="eli-tabela__table">
|
|
<EliTabelaHead
|
|
:colunas="colunasEfetivas"
|
|
:temAcoes="temAcoes"
|
|
:temColunasInvisiveis="temColunasInvisiveis"
|
|
:colunaOrdenacao="colunaOrdenacao"
|
|
:direcaoOrdenacao="direcaoOrdenacao"
|
|
@alternar-ordenacao="alternarOrdenacao"
|
|
/>
|
|
|
|
<EliTabelaBody
|
|
:colunas="colunasEfetivas"
|
|
:colunasInvisiveis="colunasInvisiveisEfetivas"
|
|
:temColunasInvisiveis="temColunasInvisiveis"
|
|
:linhasExpandidas="linhasExpandidas"
|
|
:linhas="linhasPaginadas"
|
|
:temAcoes="temAcoes"
|
|
:menuAberto="menuAberto"
|
|
:possuiAcoes="possuiAcoes"
|
|
:toggleMenu="toggleMenu"
|
|
:alternarLinhaExpandida="alternarLinhaExpandida"
|
|
/>
|
|
</table>
|
|
|
|
<EliTabelaMenuAcoes
|
|
ref="menuPopup"
|
|
:menuAberto="menuAberto"
|
|
:posicao="menuPopupPos"
|
|
:acoes="menuAberto === null ? [] : acoesDisponiveisPorLinha(menuAberto)"
|
|
:linha="menuAberto === null ? null : linhasPaginadas[menuAberto]"
|
|
@executar="({ acao, linha }) => { menuAberto = null; acao.acao(linha as never); }"
|
|
/>
|
|
|
|
<EliTabelaPaginacao
|
|
v-if="totalPaginas > 1 && quantidadeFiltrada > 0"
|
|
:pagina="paginaAtual"
|
|
:totalPaginas="totalPaginas"
|
|
:maximoBotoes="tabela.maximo_botoes_paginacao"
|
|
@alterar="irParaPagina"
|
|
/>
|
|
</template>
|
|
</div>
|
|
</template>
|
|
|
|
<script lang="ts">
|
|
/**
|
|
* EliTabela
|
|
* Componente de tabela consultável com busca, paginação, ordenação e ações por linha.
|
|
*/
|
|
|
|
/** Dependências do Vue (Composition API) */
|
|
import {
|
|
computed,
|
|
defineComponent,
|
|
onBeforeUnmount,
|
|
onMounted,
|
|
PropType,
|
|
ref,
|
|
watch,
|
|
} from "vue";
|
|
/** Enum de códigos de resposta utilizado na consulta */
|
|
import { codigosResposta } from "p-respostas";
|
|
/** Componentes auxiliares */
|
|
import EliTabelaCabecalho from "./EliTabelaCabecalho.vue";
|
|
import EliTabelaEstados from "./EliTabelaEstados.vue";
|
|
import EliTabelaDebug from "./EliTabelaDebug.vue";
|
|
import EliTabelaHead from "./EliTabelaHead.vue";
|
|
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,
|
|
components: {
|
|
EliTabelaCabecalho,
|
|
EliTabelaEstados,
|
|
EliTabelaDebug,
|
|
EliTabelaHead,
|
|
EliTabelaBody,
|
|
EliTabelaMenuAcoes,
|
|
EliTabelaPaginacao,
|
|
EliTabelaModalColunas,
|
|
EliTabelaModalFiltroAvancado,
|
|
},
|
|
props: {
|
|
/** Configuração principal da tabela (colunas, consulta e ações) */
|
|
tabela: {
|
|
type: Object as PropType<EliTabelaConsulta<any>>,
|
|
required: true,
|
|
},
|
|
},
|
|
setup(props) {
|
|
/** Flag para habilitar elementos de debug */
|
|
const isDev = import.meta.env.DEV;
|
|
/** Estados de carregamento/erro e dados retornados */
|
|
const carregando = ref(false);
|
|
const erro = ref<string | null>(null);
|
|
const linhas = ref<unknown[]>([]);
|
|
const quantidade = ref<number>(0);
|
|
|
|
/** Controle de visibilidade das ações por linha */
|
|
const acoesVisiveis = ref<boolean[][]>([]);
|
|
/** Estado do menu de ações (aberto, elemento e posição) */
|
|
const menuAberto = ref<number | null>(null);
|
|
// O componente EliTabelaMenuAcoes expõe `menuEl` (ref do elemento <ul>)
|
|
const menuPopup = ref<{ menuEl: { value: HTMLElement | null } } | null>(null);
|
|
const menuPopupPos = ref({ top: 0, left: 0 });
|
|
|
|
/** Filtros e ordenação */
|
|
const valorBusca = ref<string>("");
|
|
const paginaAtual = ref(1);
|
|
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);
|
|
|
|
/** Exibição da busca e ações do cabeçalho */
|
|
const exibirBusca = computed(() => Boolean(props.tabela.mostrarCaixaDeBusca));
|
|
const acoesCabecalho = computed(() => props.tabela.acoesTabela ?? []);
|
|
const temAcoesCabecalho = computed(() => acoesCabecalho.value.length > 0);
|
|
|
|
/** Colunas: visibilidade/ordem com persistência */
|
|
const modalColunasAberto = ref(false);
|
|
const configColunas = ref<EliTabelaColunasConfig>(
|
|
carregarConfigColunas(props.tabela.nome)
|
|
);
|
|
|
|
/** Linhas expandidas (para exibir colunas invisíveis) */
|
|
const linhasExpandidas = ref<Record<number, boolean>>({});
|
|
|
|
const rotulosColunas = computed(() => props.tabela.colunas.map((c) => c.rotulo));
|
|
|
|
const colunasInvisiveisEfetivas = computed(() => {
|
|
const colunas = props.tabela.colunas as Array<EliColuna<any>>;
|
|
|
|
const configTemDados =
|
|
(configColunas.value.visiveis?.length ?? 0) > 0 ||
|
|
(configColunas.value.invisiveis?.length ?? 0) > 0;
|
|
|
|
const invisiveisBaseRotulos = configTemDados
|
|
? configColunas.value.invisiveis ?? []
|
|
: colunas.filter((c) => c.visivel === false).map((c) => c.rotulo);
|
|
|
|
const invisiveisSet = new Set(invisiveisBaseRotulos);
|
|
const base = colunas.filter((c) => invisiveisSet.has(c.rotulo));
|
|
|
|
// ordenação: usa a lista (salva ou derivada do default) e adiciona novas ao final
|
|
const ordemSalva = invisiveisBaseRotulos;
|
|
const mapa = new Map<string, EliColuna<any>>();
|
|
for (const c of base) {
|
|
if (!mapa.has(c.rotulo)) mapa.set(c.rotulo, c);
|
|
}
|
|
|
|
const ordenadas: Array<EliColuna<any>> = [];
|
|
for (const r of ordemSalva) {
|
|
const c = mapa.get(r);
|
|
if (c) ordenadas.push(c);
|
|
}
|
|
for (const c of base) {
|
|
if (!ordenadas.includes(c)) ordenadas.push(c);
|
|
}
|
|
|
|
return ordenadas;
|
|
});
|
|
|
|
const temColunasInvisiveis = computed(() => colunasInvisiveisEfetivas.value.length > 0);
|
|
|
|
const colunasEfetivas = computed(() => {
|
|
const colunas = props.tabela.colunas;
|
|
const todosRotulos = rotulosColunas.value;
|
|
|
|
const configTemDados =
|
|
(configColunas.value.visiveis?.length ?? 0) > 0 ||
|
|
(configColunas.value.invisiveis?.length ?? 0) > 0;
|
|
|
|
const invisiveisBaseRotulos = configTemDados
|
|
? configColunas.value.invisiveis ?? []
|
|
: (props.tabela.colunas as Array<EliColuna<any>>)
|
|
.filter((c) => c.visivel === false)
|
|
.map((c) => c.rotulo);
|
|
|
|
const invisiveisSet = new Set(invisiveisBaseRotulos);
|
|
|
|
// base visiveis: remove invisiveis
|
|
const visiveisBaseRotulos = todosRotulos.filter((r) => !invisiveisSet.has(r));
|
|
const visiveisSet = new Set(visiveisBaseRotulos);
|
|
|
|
// aplica ordem salva; novas (sem definicao) entram no fim, respeitando ordem original
|
|
const ordemSalva = configTemDados ? configColunas.value.visiveis ?? [] : [];
|
|
const ordemFinal: string[] = [];
|
|
|
|
for (const r of ordemSalva) {
|
|
if (visiveisSet.has(r)) ordemFinal.push(r);
|
|
}
|
|
for (const r of visiveisBaseRotulos) {
|
|
if (!ordemFinal.includes(r)) ordemFinal.push(r);
|
|
}
|
|
|
|
// mapeia rótulo -> coluna, preservando duplicatas (se existirem) pelo primeiro match.
|
|
// OBS: pressupoe rotulo unico; se repetir, comportamento fica indefinido.
|
|
const mapa = new Map<string, any>();
|
|
for (const c of colunas) {
|
|
if (!mapa.has(c.rotulo)) mapa.set(c.rotulo, c);
|
|
}
|
|
|
|
return ordemFinal.map((r) => mapa.get(r)).filter(Boolean);
|
|
});
|
|
|
|
function abrirModalColunas() {
|
|
modalColunasAberto.value = true;
|
|
}
|
|
|
|
function fecharModalColunas() {
|
|
modalColunasAberto.value = false;
|
|
}
|
|
|
|
function salvarModalColunas(cfg: EliTabelaColunasConfig) {
|
|
configColunas.value = cfg;
|
|
salvarConfigColunas(props.tabela.nome, cfg);
|
|
modalColunasAberto.value = false;
|
|
|
|
// ao mudar colunas, fecha detalhes expandidos
|
|
linhasExpandidas.value = {};
|
|
}
|
|
|
|
function alternarLinhaExpandida(indice: number) {
|
|
const atual = Boolean(linhasExpandidas.value[indice]);
|
|
linhasExpandidas.value = {
|
|
...linhasExpandidas.value,
|
|
[indice]: !atual,
|
|
};
|
|
}
|
|
|
|
/** Registros por consulta (normaliza para inteiro positivo) */
|
|
const registrosPorConsulta = computed(() => {
|
|
const valor = props.tabela.registros_por_consulta;
|
|
if (typeof valor === "number" && valor > 0) {
|
|
return Math.floor(valor);
|
|
}
|
|
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 */
|
|
const totalPaginas = computed(() => {
|
|
const limite = registrosPorConsulta.value;
|
|
if (!limite || limite <= 0) return 1;
|
|
|
|
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;
|
|
|
|
/** Calcula a posição do menu de ações na viewport */
|
|
function atualizarPosicaoMenu(anchor: HTMLElement) {
|
|
const rect = anchor.getBoundingClientRect();
|
|
const gap = 8;
|
|
|
|
// Alinha no canto inferior direito do botão.
|
|
// Se estourar a tela para baixo, abre para cima.
|
|
const alturaMenu = menuPopup.value?.menuEl?.value?.offsetHeight ?? 0;
|
|
const larguraMenu = menuPopup.value?.menuEl?.value?.offsetWidth ?? 180;
|
|
|
|
let top = rect.bottom + gap;
|
|
const left = rect.right - larguraMenu;
|
|
|
|
if (alturaMenu && top + alturaMenu > window.innerHeight - gap) {
|
|
top = rect.top - gap - alturaMenu;
|
|
}
|
|
|
|
menuPopupPos.value = {
|
|
top: Math.max(gap, Math.round(top)),
|
|
left: Math.max(gap, Math.round(left)),
|
|
};
|
|
}
|
|
|
|
/** Fecha o menu quando ocorre clique fora */
|
|
function handleClickFora(evento: MouseEvent) {
|
|
if (menuAberto.value === null) return;
|
|
|
|
const alvo = evento.target as Node;
|
|
if (menuPopup.value?.menuEl?.value && menuPopup.value.menuEl.value.contains(alvo)) return;
|
|
|
|
if (import.meta.env.DEV) {
|
|
// eslint-disable-next-line no-console
|
|
console.log("[EliTabela] click fora => fechar menu", { menuAberto: menuAberto.value });
|
|
}
|
|
|
|
menuAberto.value = null;
|
|
}
|
|
|
|
/** Alterna ordenação e recarrega os dados */
|
|
function alternarOrdenacao(chave?: string) {
|
|
if (!chave) return;
|
|
|
|
if (colunaOrdenacao.value === chave) {
|
|
direcaoOrdenacao.value = direcaoOrdenacao.value === "asc" ? "desc" : "asc";
|
|
void carregar();
|
|
return;
|
|
}
|
|
|
|
colunaOrdenacao.value = chave;
|
|
direcaoOrdenacao.value = "asc";
|
|
if (paginaAtual.value !== 1) {
|
|
paginaAtual.value = 1;
|
|
} else {
|
|
void carregar();
|
|
}
|
|
}
|
|
|
|
/** Atualiza a busca e reinicia paginação, se necessário */
|
|
function atualizarBusca(texto: string) {
|
|
if (valorBusca.value === texto) return;
|
|
|
|
valorBusca.value = texto;
|
|
if (paginaAtual.value !== 1) {
|
|
paginaAtual.value = 1;
|
|
} else {
|
|
void carregar();
|
|
}
|
|
}
|
|
|
|
/** Navega para a página solicitada com limites */
|
|
function irParaPagina(pagina: number) {
|
|
const alvo = Math.min(Math.max(1, pagina), totalPaginas.value);
|
|
if (alvo !== paginaAtual.value) {
|
|
paginaAtual.value = alvo;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Lista ações visíveis por linha, respeitando regras sync/async de `exibir`.
|
|
*/
|
|
function acoesDisponiveisPorLinha(i: number) {
|
|
const acoesLinha = props.tabela.acoesLinha ?? [];
|
|
const visibilidade = acoesVisiveis.value[i] ?? [];
|
|
|
|
return acoesLinha
|
|
.map((acao, indice) => {
|
|
const fallbackVisivel =
|
|
acao.exibir === undefined
|
|
? true
|
|
: typeof acao.exibir === "boolean"
|
|
? acao.exibir
|
|
: false;
|
|
|
|
return {
|
|
acao,
|
|
indice,
|
|
visivel: visibilidade[indice] ?? fallbackVisivel,
|
|
};
|
|
})
|
|
.filter((item) => item.visivel);
|
|
}
|
|
|
|
/** Informa se a linha possui ações disponíveis */
|
|
function possuiAcoes(i: number) {
|
|
return acoesDisponiveisPorLinha(i).length > 0;
|
|
}
|
|
|
|
/** Abre/fecha o menu de ações da linha e posiciona o popup */
|
|
function toggleMenu(i: number, evento?: MouseEvent) {
|
|
if (!possuiAcoes(i)) return;
|
|
|
|
if (import.meta.env.DEV) {
|
|
// eslint-disable-next-line no-console
|
|
console.log("[EliTabela] toggleMenu (antes)", { i, atual: menuAberto.value });
|
|
}
|
|
|
|
if (menuAberto.value === i) {
|
|
menuAberto.value = null;
|
|
|
|
if (import.meta.env.DEV) {
|
|
// eslint-disable-next-line no-console
|
|
console.log("[EliTabela] toggleMenu => fechou", { i });
|
|
}
|
|
return;
|
|
}
|
|
|
|
menuAberto.value = i;
|
|
|
|
if (import.meta.env.DEV) {
|
|
// eslint-disable-next-line no-console
|
|
console.log("[EliTabela] toggleMenu => abriu", { i });
|
|
}
|
|
|
|
// posiciona assim que abrir
|
|
const anchor = (evento?.currentTarget as HTMLElement | null) ?? null;
|
|
if (anchor) {
|
|
// primeiro posicionamento (antes do menu medir)
|
|
atualizarPosicaoMenu(anchor);
|
|
// reposiciona no próximo frame para pegar altura real
|
|
requestAnimationFrame(() => atualizarPosicaoMenu(anchor));
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Executa consulta, aplica filtros e resolve visibilidade de ações.
|
|
* Usa o contador sequencial para ignorar respostas antigas.
|
|
*/
|
|
async function carregar() {
|
|
const idCarregamento = ++carregamentoSequencial;
|
|
carregando.value = true;
|
|
erro.value = null;
|
|
acoesVisiveis.value = [];
|
|
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 parametrosConsulta: {
|
|
coluna_ordem?: never;
|
|
direcao_ordem?: "asc" | "desc";
|
|
offSet: number;
|
|
limit: number;
|
|
texto_busca?: string;
|
|
} = {
|
|
offSet: offset,
|
|
limit: 999999,
|
|
};
|
|
|
|
// texto_busca ficará somente para filtragem local.
|
|
|
|
if (colunaOrdenacao.value) {
|
|
parametrosConsulta.coluna_ordem = colunaOrdenacao.value as never;
|
|
parametrosConsulta.direcao_ordem = direcaoOrdenacao.value;
|
|
}
|
|
|
|
try {
|
|
const tabelaConfig = props.tabela;
|
|
const res = await tabelaConfig.consulta(parametrosConsulta);
|
|
|
|
if (idCarregamento !== carregamentoSequencial) return;
|
|
|
|
if (res.cod !== codigosResposta.sucesso) {
|
|
linhas.value = [];
|
|
quantidade.value = 0;
|
|
erro.value = res.mensagem;
|
|
return;
|
|
}
|
|
|
|
const valores = res.valor?.valores ?? [];
|
|
const total = valores.length;
|
|
|
|
linhas.value = valores;
|
|
quantidade.value = total;
|
|
|
|
const totalPaginasRecalculado = Math.max(1, Math.ceil((quantidadeFiltrada.value || 0) / limite));
|
|
if (paginaAtual.value > totalPaginasRecalculado) {
|
|
paginaAtual.value = totalPaginasRecalculado;
|
|
return;
|
|
}
|
|
|
|
const acoesLinhaConfiguradas = tabelaConfig.acoesLinha ?? [];
|
|
if (!acoesLinhaConfiguradas.length) {
|
|
acoesVisiveis.value = [];
|
|
return;
|
|
}
|
|
|
|
const preResultado = valores.map(() =>
|
|
acoesLinhaConfiguradas.map((acao) => {
|
|
if (acao.exibir === undefined) return true;
|
|
if (typeof acao.exibir === "boolean") return acao.exibir;
|
|
return false;
|
|
})
|
|
);
|
|
|
|
acoesVisiveis.value = preResultado;
|
|
|
|
const visibilidade = await Promise.all(
|
|
valores.map(async (linha) =>
|
|
Promise.all(
|
|
acoesLinhaConfiguradas.map(async (acao) => {
|
|
if (acao.exibir === undefined) return true;
|
|
if (typeof acao.exibir === "boolean") return acao.exibir;
|
|
|
|
try {
|
|
const resultado = acao.exibir(linha as never);
|
|
return Boolean(await Promise.resolve(resultado));
|
|
} catch {
|
|
return false;
|
|
}
|
|
})
|
|
)
|
|
)
|
|
);
|
|
|
|
if (idCarregamento === carregamentoSequencial) {
|
|
acoesVisiveis.value = visibilidade;
|
|
}
|
|
} catch (e) {
|
|
if (idCarregamento !== carregamentoSequencial) return;
|
|
|
|
linhas.value = [];
|
|
quantidade.value = 0;
|
|
erro.value = e instanceof Error ? e.message : "Erro ao carregar dados.";
|
|
} finally {
|
|
if (idCarregamento === carregamentoSequencial) {
|
|
carregando.value = false;
|
|
}
|
|
}
|
|
}
|
|
|
|
/** Ciclo de vida: registra listener global e carrega dados iniciais */
|
|
onMounted(() => {
|
|
document.addEventListener("click", handleClickFora);
|
|
void carregar();
|
|
});
|
|
|
|
/** Ciclo de vida: remove listener global */
|
|
onBeforeUnmount(() => {
|
|
document.removeEventListener("click", handleClickFora);
|
|
});
|
|
|
|
/** Watch: ao desabilitar busca, limpa termo e recarrega */
|
|
watch(
|
|
() => props.tabela.mostrarCaixaDeBusca,
|
|
(mostrar) => {
|
|
if (!mostrar && valorBusca.value) {
|
|
valorBusca.value = "";
|
|
if (paginaAtual.value !== 1) {
|
|
paginaAtual.value = 1;
|
|
} else {
|
|
void carregar();
|
|
}
|
|
}
|
|
}
|
|
);
|
|
|
|
/** Watch: mudança de página dispara nova consulta */
|
|
watch(paginaAtual, (nova, antiga) => {
|
|
// paginação local não precisa recarregar
|
|
if (nova !== antiga) {
|
|
// noop
|
|
}
|
|
});
|
|
|
|
/** Watch: troca de configuração reseta estados e recarrega */
|
|
watch(
|
|
() => props.tabela,
|
|
() => {
|
|
menuAberto.value = null;
|
|
colunaOrdenacao.value = null;
|
|
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;
|
|
} else {
|
|
void carregar();
|
|
}
|
|
}
|
|
);
|
|
|
|
/** Watch: alteração do limite de registros reinicia paginação */
|
|
watch(
|
|
() => props.tabela.registros_por_consulta,
|
|
() => {
|
|
if (paginaAtual.value !== 1) {
|
|
paginaAtual.value = 1;
|
|
} else {
|
|
void carregar();
|
|
}
|
|
}
|
|
);
|
|
|
|
/** Watch: mudança nas linhas fecha o menu aberto */
|
|
watch(linhas, () => {
|
|
menuAberto.value = null;
|
|
linhasExpandidas.value = {};
|
|
});
|
|
|
|
/** Exposição para o template (state, computed, helpers e actions) */
|
|
return {
|
|
// state
|
|
isDev,
|
|
tabela,
|
|
carregando,
|
|
erro,
|
|
linhas,
|
|
linhasPaginadas,
|
|
quantidadeFiltrada,
|
|
quantidade,
|
|
menuAberto,
|
|
valorBusca,
|
|
paginaAtual,
|
|
colunaOrdenacao,
|
|
direcaoOrdenacao,
|
|
totalPaginas,
|
|
|
|
// computed
|
|
exibirBusca,
|
|
exibirFiltroAvancado,
|
|
acoesCabecalho,
|
|
temAcoesCabecalho,
|
|
temAcoes,
|
|
colunasEfetivas,
|
|
rotulosColunas,
|
|
modalColunasAberto,
|
|
configColunas,
|
|
|
|
temColunasInvisiveis,
|
|
colunasInvisiveisEfetivas,
|
|
linhasExpandidas,
|
|
|
|
abrirModalColunas,
|
|
abrirModalFiltro,
|
|
fecharModalColunas,
|
|
salvarModalColunas,
|
|
|
|
modalFiltroAberto,
|
|
filtrosUi,
|
|
salvarFiltrosAvancados,
|
|
limparFiltrosAvancados,
|
|
fecharModalFiltro,
|
|
|
|
alternarLinhaExpandida,
|
|
|
|
// actions
|
|
alternarOrdenacao,
|
|
atualizarBusca,
|
|
irParaPagina,
|
|
acoesDisponiveisPorLinha,
|
|
possuiAcoes,
|
|
toggleMenu,
|
|
|
|
// popup
|
|
menuPopup,
|
|
menuPopupPos,
|
|
};
|
|
},
|
|
});
|
|
</script>
|
|
|
|
<style src="./EliTabela.css"></style>
|