vue-componentes/src/componentes/EliTabela/EliTabelaModalFiltroAvancado.vue
2026-01-29 15:33:42 -03:00

391 lines
10 KiB
Vue
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.

<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="String(linha.coluna)" class="eli-tabela-modal-filtro__linha">
<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">
<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) }}
</option>
</select>
<button
type="button"
class="eli-tabela-modal-filtro__botao"
@click="adicionar"
:disabled="!colunaParaAdicionar"
>
Adicionar
</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 { computed, defineComponent, PropType, ref, watch } from "vue";
import { EliEntradaTexto, EliEntradaNumero, EliEntradaDataHora } from "../EliEntrada";
import type { ComponenteEntrada, TipoEntrada } from "../EliEntrada/tiposEntradas";
import type { EliTabelaConsulta } from "./types-eli-tabela";
type FiltroBase<T> = NonNullable<EliTabelaConsulta<T>["filtroAvancado"]>[number];
type LinhaFiltro<T> = {
coluna: keyof T;
entrada: ComponenteEntrada;
operador: string;
valor: any;
};
function isTipoEntrada(v: unknown): v is TipoEntrada {
return v === "texto" || v === "numero" || v === "dataHora";
}
function rotuloDoFiltro(f: FiltroBase<any>) {
const rotulo = (f?.entrada?.[1] as any)?.rotulo;
return rotulo ? String(rotulo) : String(f?.coluna ?? "Filtro");
}
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 colunaParaAdicionar = ref<string>("");
const colunasDisponiveis = computed(() => (props.filtrosBase ?? []).map((b) => String(b.coluna)));
const opcoesParaAdicionar = computed(() => {
const usadas = new Set(linhas.value.map((l) => String(l.coluna)));
return (props.filtrosBase ?? []).filter((b) => !usadas.has(String(b.coluna)));
});
function componenteEntrada(entrada: ComponenteEntrada) {
const tipo = entrada?.[0];
if (tipo === "numero") return EliEntradaNumero;
if (tipo === "dataHora") return EliEntradaDataHora;
return EliEntradaTexto;
}
function opcoesEntrada(entrada: ComponenteEntrada) {
// o rótulo vem do próprio componente (entrada[1].rotulo)
return (entrada?.[1] as any) ?? { rotulo: "" };
}
function valorInicialPorEntrada(entrada: ComponenteEntrada) {
const tipo = entrada?.[0];
if (tipo === "numero") return null;
return "";
}
function normalizarModelo() {
const base = props.filtrosBase ?? [];
const modelo = Array.isArray(props.modelo) ? props.modelo : [];
linhas.value = modelo.map((m: any) => {
// operador vem travado no base
const baseItem = base.find((b) => String(b.coluna) === String(m.coluna)) ?? base[0];
const entrada = (baseItem?.entrada ?? m.entrada) as ComponenteEntrada;
const col = (baseItem?.coluna ?? m.coluna) as any;
const op = String(baseItem?.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
// não auto-adiciona; usuário escolhe quais filtros quer usar
// se algum filtro mudou a coluna para valor inválido, ajusta
for (const l of linhas.value) {
if (!colunasDisponiveis.value.includes(String(l.coluna))) continue;
l.operador = String((base.find((b) => String(b.coluna) === String(l.coluna))?.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 (!colunaParaAdicionar.value) return;
const b0 = (props.filtrosBase ?? []).find((b) => String(b.coluna) === String(colunaParaAdicionar.value));
if (!b0) return;
// evita repetição
if (linhas.value.some((l) => String(l.coluna) === String(b0.coluna))) return;
linhas.value.push({
coluna: b0.coluna as any,
entrada: b0.entrada,
operador: String(b0.operador ?? "="),
valor: valorInicialPorEntrada(b0.entrada),
});
colunaParaAdicionar.value = "";
}
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,
valor: l.valor,
}))
);
}
return {
linhas,
opcoesParaAdicionar,
colunaParaAdicionar,
componenteEntrada,
opcoesEntrada,
adicionar,
remover,
// exibimos operador fixo só como texto
emitFechar,
emitSalvar,
emitLimpar,
rotuloDoFiltro,
};
},
});
</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: 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>