bkp
This commit is contained in:
parent
0144788548
commit
e7357e064a
19 changed files with 14478 additions and 1364 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -10,6 +10,15 @@
|
|||
>
|
||||
Colunas
|
||||
</button>
|
||||
|
||||
<button
|
||||
v-if="exibirBotaoFiltroAvancado"
|
||||
type="button"
|
||||
class="eli-tabela__acoes-cabecalho-botao eli-tabela__acoes-cabecalho-botao--filtro"
|
||||
@click="emitFiltroAvancado"
|
||||
>
|
||||
Filtro
|
||||
</button>
|
||||
<EliTabelaCaixaDeBusca :modelo="valorBusca" @buscar="emitBuscar" />
|
||||
</div>
|
||||
|
||||
|
|
@ -53,6 +62,11 @@ export default defineComponent({
|
|||
required: false,
|
||||
default: true,
|
||||
},
|
||||
exibirBotaoFiltroAvancado: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false,
|
||||
},
|
||||
valorBusca: {
|
||||
type: String,
|
||||
required: true,
|
||||
|
|
@ -76,6 +90,9 @@ export default defineComponent({
|
|||
colunas() {
|
||||
return true;
|
||||
},
|
||||
filtroAvancado() {
|
||||
return true;
|
||||
},
|
||||
},
|
||||
setup(props, { emit }) {
|
||||
const temAcoesCabecalho = computed(() => props.acoesCabecalho.length > 0);
|
||||
|
|
@ -88,7 +105,11 @@ export default defineComponent({
|
|||
emit("colunas");
|
||||
}
|
||||
|
||||
return { temAcoesCabecalho, emitBuscar, emitColunas };
|
||||
function emitFiltroAvancado() {
|
||||
emit("filtroAvancado");
|
||||
}
|
||||
|
||||
return { temAcoesCabecalho, emitBuscar, emitColunas, emitFiltroAvancado };
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
|
|
|||
397
src/componentes/EliTabela/EliTabelaModalFiltroAvancado.vue
Normal file
397
src/componentes/EliTabela/EliTabelaModalFiltroAvancado.vue
Normal file
|
|
@ -0,0 +1,397 @@
|
|||
<template>
|
||||
<div v-if="aberto" class="eli-tabela-modal-filtro__overlay" role="presentation" @click.self="emitFechar">
|
||||
<div class="eli-tabela-modal-filtro__modal" role="dialog" aria-modal="true" aria-label="Filtro avançado">
|
||||
<header class="eli-tabela-modal-filtro__header">
|
||||
<h3 class="eli-tabela-modal-filtro__titulo">Filtro avançado</h3>
|
||||
<button type="button" class="eli-tabela-modal-filtro__fechar" aria-label="Fechar" @click="emitFechar">
|
||||
×
|
||||
</button>
|
||||
</header>
|
||||
|
||||
<div class="eli-tabela-modal-filtro__conteudo">
|
||||
<div v-if="!filtrosBase.length" class="eli-tabela-modal-filtro__vazio">
|
||||
Nenhum filtro configurado na tabela.
|
||||
</div>
|
||||
|
||||
<div v-else class="eli-tabela-modal-filtro__lista">
|
||||
<div v-for="(linha, idx) in linhas" :key="idx" class="eli-tabela-modal-filtro__linha">
|
||||
<select v-model="linha.coluna" class="eli-tabela-modal-filtro__select">
|
||||
<option v-for="opt in colunasDisponiveis" :key="String(opt)" :value="opt">
|
||||
{{ String(opt) }}
|
||||
</option>
|
||||
</select>
|
||||
|
||||
<select v-model="linha.operador" class="eli-tabela-modal-filtro__select">
|
||||
<option v-for="op in operadoresDisponiveis" :key="op" :value="op">
|
||||
{{ op }}
|
||||
</option>
|
||||
</select>
|
||||
|
||||
<div class="eli-tabela-modal-filtro__entrada">
|
||||
<component
|
||||
:is="componenteEntrada(linha.entrada)"
|
||||
v-model:value="linha.valor"
|
||||
:opcoes="opcoesEntrada(linha.entrada)"
|
||||
density="compact"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
class="eli-tabela-modal-filtro__remover"
|
||||
title="Remover"
|
||||
aria-label="Remover"
|
||||
@click="remover(idx)"
|
||||
>
|
||||
×
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="eli-tabela-modal-filtro__acoes">
|
||||
<button type="button" class="eli-tabela-modal-filtro__botao" @click="adicionar" :disabled="!filtrosBase.length">
|
||||
Adicionar filtro
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<footer class="eli-tabela-modal-filtro__footer">
|
||||
<button type="button" class="eli-tabela-modal-filtro__botao eli-tabela-modal-filtro__botao--sec" @click="emitLimpar">
|
||||
Limpar
|
||||
</button>
|
||||
<button type="button" class="eli-tabela-modal-filtro__botao eli-tabela-modal-filtro__botao--sec" @click="emitFechar">
|
||||
Cancelar
|
||||
</button>
|
||||
<button type="button" class="eli-tabela-modal-filtro__botao eli-tabela-modal-filtro__botao--prim" @click="emitSalvar">
|
||||
Aplicar
|
||||
</button>
|
||||
</footer>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, PropType, ref, watch } from "vue";
|
||||
import { operadores as Operadores } from "p-comuns";
|
||||
|
||||
import { EliEntradaTexto, EliEntradaNumero, EliEntradaDataHora } from "../EliEntrada";
|
||||
import type { ComponenteEntrada, TipoEntrada } from "../EliEntrada/tiposEntradas";
|
||||
import type { EliTabelaConsulta } from "./types-eli-tabela";
|
||||
|
||||
type Operador = keyof typeof Operadores;
|
||||
type FiltroBase<T> = NonNullable<EliTabelaConsulta<T>["filtroAvancado"]>[number];
|
||||
|
||||
type LinhaFiltro<T> = {
|
||||
coluna: keyof T;
|
||||
operador: Operador;
|
||||
entrada: ComponenteEntrada;
|
||||
valor: any;
|
||||
};
|
||||
|
||||
function isTipoEntrada(v: unknown): v is TipoEntrada {
|
||||
return v === "texto" || v === "numero" || v === "dataHora";
|
||||
}
|
||||
|
||||
export default defineComponent({
|
||||
name: "EliTabelaModalFiltroAvancado",
|
||||
props: {
|
||||
aberto: { type: Boolean, required: true },
|
||||
filtrosBase: {
|
||||
type: Array as PropType<Array<FiltroBase<any>>>,
|
||||
required: true,
|
||||
},
|
||||
modelo: {
|
||||
type: Array as PropType<Array<any>>,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
emits: {
|
||||
fechar: () => true,
|
||||
limpar: () => true,
|
||||
salvar: (_linhas: any[]) => true,
|
||||
},
|
||||
setup(props, { emit }) {
|
||||
const linhas = ref<Array<LinhaFiltro<any>>>([]);
|
||||
|
||||
const operadoresDisponiveis = Object.keys(Operadores) as Operador[];
|
||||
|
||||
const colunasDisponiveis = ref<string[]>([]);
|
||||
|
||||
function componenteEntrada(entrada: ComponenteEntrada) {
|
||||
const tipo = entrada?.[0];
|
||||
if (tipo === "numero") return EliEntradaNumero;
|
||||
if (tipo === "dataHora") return EliEntradaDataHora;
|
||||
return EliEntradaTexto;
|
||||
}
|
||||
|
||||
function opcoesEntrada(entrada: ComponenteEntrada) {
|
||||
const opcoes = entrada?.[1] as any;
|
||||
// garante rotulo para não ficar vazio visualmente dentro do modal
|
||||
if (opcoes && typeof opcoes === "object" && !opcoes.rotulo) {
|
||||
return { ...opcoes, rotulo: "Valor" };
|
||||
}
|
||||
return opcoes ?? { rotulo: "Valor" };
|
||||
}
|
||||
|
||||
function valorInicialPorEntrada(entrada: ComponenteEntrada) {
|
||||
const tipo = entrada?.[0];
|
||||
if (tipo === "numero") return null;
|
||||
return "";
|
||||
}
|
||||
|
||||
function normalizarModelo() {
|
||||
const base = props.filtrosBase ?? [];
|
||||
colunasDisponiveis.value = base.map((b) => String(b.coluna));
|
||||
|
||||
const modelo = Array.isArray(props.modelo) ? props.modelo : [];
|
||||
linhas.value = modelo.map((m: any) => {
|
||||
const entrada = m.entrada as ComponenteEntrada;
|
||||
const col = m.coluna as any;
|
||||
const op = (m.operador ?? "=") as Operador;
|
||||
const val = m.valor ?? valorInicialPorEntrada(entrada);
|
||||
|
||||
return {
|
||||
coluna: col,
|
||||
operador: op,
|
||||
entrada,
|
||||
valor: val,
|
||||
} as LinhaFiltro<any>;
|
||||
});
|
||||
|
||||
// se vazio e existe base, adiciona 1 linha default
|
||||
if (!linhas.value.length && base.length) {
|
||||
const b0 = base[0];
|
||||
linhas.value = [
|
||||
{
|
||||
coluna: b0.coluna as any,
|
||||
operador: (b0.operador as any) ?? "=",
|
||||
entrada: b0.entrada,
|
||||
valor: valorInicialPorEntrada(b0.entrada),
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
// se algum filtro mudou a coluna para valor inválido, ajusta
|
||||
for (const l of linhas.value) {
|
||||
if (!colunasDisponiveis.value.includes(String(l.coluna))) {
|
||||
l.coluna = (base[0]?.coluna as any) ?? l.coluna;
|
||||
}
|
||||
if (!operadoresDisponiveis.includes(l.operador)) {
|
||||
l.operador = "=";
|
||||
}
|
||||
// sanity
|
||||
if (l.entrada && !isTipoEntrada(l.entrada[0])) {
|
||||
l.entrada = ["texto", { rotulo: "Valor" }] as any;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
watch(
|
||||
() => [props.aberto, props.filtrosBase, props.modelo] as const,
|
||||
() => {
|
||||
if (props.aberto) normalizarModelo();
|
||||
},
|
||||
{ deep: true, immediate: true }
|
||||
);
|
||||
|
||||
function adicionar() {
|
||||
if (!props.filtrosBase.length) return;
|
||||
const b0 = props.filtrosBase[0];
|
||||
linhas.value.push({
|
||||
coluna: b0.coluna as any,
|
||||
operador: (b0.operador as any) ?? "=",
|
||||
entrada: b0.entrada,
|
||||
valor: valorInicialPorEntrada(b0.entrada),
|
||||
});
|
||||
}
|
||||
|
||||
function remover(idx: number) {
|
||||
linhas.value.splice(idx, 1);
|
||||
}
|
||||
|
||||
function emitFechar() {
|
||||
emit("fechar");
|
||||
}
|
||||
|
||||
function emitLimpar() {
|
||||
emit("limpar");
|
||||
}
|
||||
|
||||
function emitSalvar() {
|
||||
emit(
|
||||
"salvar",
|
||||
linhas.value.map((l) => ({
|
||||
coluna: l.coluna,
|
||||
operador: l.operador,
|
||||
entrada: l.entrada,
|
||||
valor: l.valor,
|
||||
}))
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
linhas,
|
||||
operadoresDisponiveis,
|
||||
colunasDisponiveis,
|
||||
componenteEntrada,
|
||||
opcoesEntrada,
|
||||
adicionar,
|
||||
remover,
|
||||
emitFechar,
|
||||
emitSalvar,
|
||||
emitLimpar,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.eli-tabela-modal-filtro__overlay {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
background: rgba(15, 23, 42, 0.35);
|
||||
z-index: 4000;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.eli-tabela-modal-filtro__modal {
|
||||
width: min(980px, 100%);
|
||||
background: #fff;
|
||||
border-radius: 14px;
|
||||
border: 1px solid rgba(15, 23, 42, 0.1);
|
||||
box-shadow: 0 18px 60px rgba(15, 23, 42, 0.25);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.eli-tabela-modal-filtro__header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 14px 16px;
|
||||
border-bottom: 1px solid rgba(15, 23, 42, 0.08);
|
||||
}
|
||||
|
||||
.eli-tabela-modal-filtro__titulo {
|
||||
font-size: 1rem;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.eli-tabela-modal-filtro__fechar {
|
||||
width: 34px;
|
||||
height: 34px;
|
||||
border-radius: 10px;
|
||||
border: none;
|
||||
background: transparent;
|
||||
cursor: pointer;
|
||||
font-size: 22px;
|
||||
line-height: 1;
|
||||
color: rgba(15, 23, 42, 0.8);
|
||||
}
|
||||
|
||||
.eli-tabela-modal-filtro__fechar:hover,
|
||||
.eli-tabela-modal-filtro__fechar:focus-visible {
|
||||
background: rgba(15, 23, 42, 0.06);
|
||||
}
|
||||
|
||||
.eli-tabela-modal-filtro__conteudo {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.eli-tabela-modal-filtro__vazio {
|
||||
opacity: 0.75;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.eli-tabela-modal-filtro__lista {
|
||||
display: grid;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.eli-tabela-modal-filtro__linha {
|
||||
display: grid;
|
||||
grid-template-columns: 220px 120px 1fr 34px;
|
||||
gap: 10px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.eli-tabela-modal-filtro__select {
|
||||
height: 34px;
|
||||
border-radius: 10px;
|
||||
border: 1px solid rgba(15, 23, 42, 0.12);
|
||||
padding: 0 10px;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.eli-tabela-modal-filtro__entrada {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.eli-tabela-modal-filtro__remover {
|
||||
width: 34px;
|
||||
height: 34px;
|
||||
border-radius: 10px;
|
||||
border: 1px solid rgba(15, 23, 42, 0.12);
|
||||
background: #fff;
|
||||
cursor: pointer;
|
||||
font-size: 18px;
|
||||
line-height: 1;
|
||||
color: rgba(15, 23, 42, 0.8);
|
||||
}
|
||||
|
||||
.eli-tabela-modal-filtro__remover:hover,
|
||||
.eli-tabela-modal-filtro__remover:focus-visible {
|
||||
background: rgba(15, 23, 42, 0.06);
|
||||
}
|
||||
|
||||
.eli-tabela-modal-filtro__acoes {
|
||||
margin-top: 12px;
|
||||
}
|
||||
|
||||
.eli-tabela-modal-filtro__footer {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 8px;
|
||||
padding: 14px 16px;
|
||||
border-top: 1px solid rgba(15, 23, 42, 0.08);
|
||||
}
|
||||
|
||||
.eli-tabela-modal-filtro__botao {
|
||||
height: 34px;
|
||||
padding: 0 14px;
|
||||
border-radius: 10px;
|
||||
border: 1px solid rgba(15, 23, 42, 0.12);
|
||||
background: #fff;
|
||||
cursor: pointer;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.eli-tabela-modal-filtro__botao:disabled {
|
||||
opacity: 0.55;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.eli-tabela-modal-filtro__botao--sec:hover,
|
||||
.eli-tabela-modal-filtro__botao--sec:focus-visible {
|
||||
background: rgba(15, 23, 42, 0.06);
|
||||
}
|
||||
|
||||
.eli-tabela-modal-filtro__botao--prim {
|
||||
border: none;
|
||||
background: rgba(37, 99, 235, 0.95);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.eli-tabela-modal-filtro__botao--prim:hover,
|
||||
.eli-tabela-modal-filtro__botao--prim:focus-visible {
|
||||
background: rgba(37, 99, 235, 1);
|
||||
}
|
||||
|
||||
@media (max-width: 880px) {
|
||||
.eli-tabela-modal-filtro__linha {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
34
src/componentes/EliTabela/filtroAvancadoStorage.ts
Normal file
34
src/componentes/EliTabela/filtroAvancadoStorage.ts
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
import type { EliTabelaConsulta } from "./types-eli-tabela";
|
||||
|
||||
export type EliTabelaFiltroAvancadoSalvo<T> = NonNullable<EliTabelaConsulta<T>["filtroAvancado"]>;
|
||||
|
||||
function key(nomeTabela: string) {
|
||||
return `eli_tabela:${nomeTabela}:filtro_avancado`;
|
||||
}
|
||||
|
||||
export function carregarFiltroAvancado<T>(nomeTabela: string): EliTabelaFiltroAvancadoSalvo<T> {
|
||||
try {
|
||||
const raw = localStorage.getItem(key(nomeTabela));
|
||||
if (!raw) return [] as unknown as EliTabelaFiltroAvancadoSalvo<T>;
|
||||
const parsed = JSON.parse(raw);
|
||||
return Array.isArray(parsed) ? (parsed as EliTabelaFiltroAvancadoSalvo<T>) : ([] as any);
|
||||
} catch {
|
||||
return [] as unknown as EliTabelaFiltroAvancadoSalvo<T>;
|
||||
}
|
||||
}
|
||||
|
||||
export function salvarFiltroAvancado<T>(nomeTabela: string, filtros: EliTabelaFiltroAvancadoSalvo<T>) {
|
||||
try {
|
||||
localStorage.setItem(key(nomeTabela), JSON.stringify(filtros ?? []));
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
|
||||
export function limparFiltroAvancado(nomeTabela: string) {
|
||||
try {
|
||||
localStorage.removeItem(key(nomeTabela));
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
|
|
@ -1,10 +1,12 @@
|
|||
import type { tipoResposta } from "p-respostas";
|
||||
import type { LucideIcon } from "lucide-vue-next";
|
||||
import type { TipoTabelaCelula, TiposTabelaCelulas } from "./celulas/tiposTabelaCelulas";
|
||||
import { operadores, tipoFiltro } from "p-comuns";
|
||||
import { TipoEntrada } from "../EliEntrada";
|
||||
import { operadores, zFiltro } from "p-comuns";
|
||||
import { ComponenteEntrada } from "../EliEntrada/tiposEntradas";
|
||||
|
||||
// `p-comuns` expõe `zFiltro` (schema). Inferimos o tipo a partir do `parse`.
|
||||
export type tipoFiltro = ReturnType<(typeof zFiltro)["parse"]>
|
||||
|
||||
|
||||
|
||||
|
||||
|
|
@ -130,6 +132,7 @@ export type EliTabelaConsulta<T> = {
|
|||
// 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,
|
||||
operador: operadores | keyof typeof operadores,
|
||||
entrada: ComponenteEntrada
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue