vue-componentes/src/componentes/EliTabela/EliTabelaModalFiltroAvancado.vue
2026-02-17 11:33:44 -03:00

403 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.chave)" class="eli-tabela-modal-filtro__linha">
<div class="eli-tabela-modal-filtro__entrada">
<component
:is="componenteEntrada(linha.entrada)"
:value="linha.valor"
@update:value="(v: any) => (linha.valor = v)"
@input="(v: any) => (linha.valor = v)"
: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="chaveParaAdicionar"
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.chave)" :value="String(o.chave)">
{{ rotuloDoFiltro(o) }}
</option>
</select>
<button
type="button"
class="eli-tabela-modal-filtro__botao"
@click="adicionar"
:disabled="!chaveParaAdicionar"
>
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, type PropType, ref, watch } from "vue"
import {
EliEntradaDataHora,
EliEntradaNumero,
EliEntradaTexto,
} from "../EliEntrada"
import type { ComponenteEntrada } from "../EliEntrada/tiposEntradas"
import type { tipoEliTabelaConsulta } from "./types-eli-tabela"
type FiltroBase<T> = NonNullable<
tipoEliTabelaConsulta<T>["filtroAvancado"]
>[number]
type LinhaFiltro = {
chave: string
entrada: ComponenteEntrada
// biome-ignore lint/suspicious/noExplicitAny: dynamic value
valor: any
}
// biome-ignore lint/suspicious/noExplicitAny: dynamic typing needed
function rotuloDoFiltro(f: FiltroBase<any>) {
return f.rotulo
}
export default defineComponent({
name: "EliTabelaModalFiltroAvancado",
props: {
aberto: { type: Boolean, required: true },
filtrosBase: {
// biome-ignore lint/suspicious/noExplicitAny: generic component
type: Array as PropType<Array<FiltroBase<any>>>,
required: true,
},
modelo: {
// biome-ignore lint/suspicious/noExplicitAny: dynamic model
type: Array as PropType<Array<any>>,
required: true,
},
},
emits: {
fechar: () => true,
limpar: () => true,
// biome-ignore lint/suspicious/noExplicitAny: dynamic lines
salvar: (_linhas: any[]) => true,
},
setup(props, { emit }) {
const linhas = ref<Array<LinhaFiltro>>([])
const chaveParaAdicionar = ref<string>("")
const chavesDisponiveis = computed(() =>
(props.filtrosBase ?? []).map((b) => String(b.chave)),
)
const opcoesParaAdicionar = computed(() => {
const usadas = new Set(linhas.value.map((l) => String(l.chave)))
return (props.filtrosBase ?? []).filter(
(b) => !usadas.has(String(b.chave)),
)
})
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)
// biome-ignore lint/suspicious/noExplicitAny: dynamic options
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
// biome-ignore lint/suspicious/noExplicitAny: dynamic model item
.map((m: any) => {
const baseItem =
base.find((b) => String(b.chave) === String(m.chave)) ?? base[0]
if (!baseItem) return null
const entrada = (baseItem.entrada ?? m.entrada) as ComponenteEntrada
const chave = String(baseItem.chave ?? m.chave)
const val = m.valor ?? valorInicialPorEntrada(entrada)
return {
chave: chave,
entrada,
valor: val,
} as LinhaFiltro
})
.filter(Boolean) as LinhaFiltro[]
// se algum filtro mudou a chave para valor inválido, ajusta
linhas.value = linhas.value.filter((l) =>
chavesDisponiveis.value.includes(String(l.chave)),
)
}
watch(
() => [props.aberto, props.filtrosBase, props.modelo] as const,
() => {
if (props.aberto) normalizarModelo()
},
{ deep: true, immediate: true },
)
function adicionar() {
if (!chaveParaAdicionar.value) return
const b0 = (props.filtrosBase ?? []).find(
(b) => String(b.chave) === String(chaveParaAdicionar.value),
)
if (!b0) return
// evita repetição
if (linhas.value.some((l) => String(l.chave) === String(b0.chave))) return
linhas.value.push({
chave: String(b0.chave),
entrada: b0.entrada,
valor: valorInicialPorEntrada(b0.entrada),
})
chaveParaAdicionar.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) => ({
chave: l.chave,
valor: l.valor,
})),
)
}
return {
linhas,
opcoesParaAdicionar,
chaveParaAdicionar,
componenteEntrada,
opcoesEntrada,
adicionar,
remover,
emitFechar,
emitSalvar,
emitLimpar,
rotuloDoFiltro,
}
},
})
</script>
<style>
.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>