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