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