Atualização floresta
This commit is contained in:
parent
0886fd5c3e
commit
2d4d8f8929
103 changed files with 19266 additions and 14160 deletions
|
|
@ -1,403 +1,182 @@
|
|||
<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>
|
||||
<Transition name="eli-tabela-modal-filtro">
|
||||
<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">
|
||||
<div class="eli-tabela-modal-filtro__header-titulo">
|
||||
<Filter class="eli-tabela-modal-filtro__header-icone" :size="18" :stroke-width="2" />
|
||||
<h3 class="eli-tabela-modal-filtro__titulo">Filtro avançado</h3>
|
||||
<span v-if="linhas.length" class="eli-tabela-modal-filtro__contador">
|
||||
{{ linhas.length }} {{ linhas.length === 1 ? 'filtro' : 'filtros' }}
|
||||
</span>
|
||||
</div>
|
||||
<button type="button" class="eli-tabela-modal-filtro__fechar" aria-label="Fechar" @click="emitFechar">
|
||||
<X :size="18" :stroke-width="2" />
|
||||
</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 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 v-else>
|
||||
<!-- Lista de filtros ativos -->
|
||||
<div v-if="linhas.length" 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__linha-header">
|
||||
<span
|
||||
class="eli-tabela-modal-filtro__id-badge"
|
||||
:title="`Identificador na fórmula: {c${idx + 1}}`"
|
||||
>
|
||||
c{{ idx + 1 }}
|
||||
</span>
|
||||
<span class="eli-tabela-modal-filtro__rotulo-filtro">{{ rotuloDoFiltroAtivo(linha.chave) }}</span>
|
||||
<button
|
||||
type="button"
|
||||
class="eli-tabela-modal-filtro__remover"
|
||||
title="Remover filtro"
|
||||
aria-label="Remover filtro"
|
||||
@click="remover(idx)"
|
||||
>
|
||||
<Trash2 :size="14" :stroke-width="2" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="eli-tabela-modal-filtro__entrada">
|
||||
<component
|
||||
:is="componenteEntrada(linha.entrada)"
|
||||
v-bind="({ value: linha.valor, opcoes: opcoesEntrada(linha.entrada), density: 'compact' } as Record<string, unknown>)"
|
||||
@update:value="(v: unknown) => (linha.valor = v)"
|
||||
@input="(v: unknown) => (linha.valor = v)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
class="eli-tabela-modal-filtro__remover"
|
||||
title="Remover"
|
||||
aria-label="Remover"
|
||||
@click="remover(idx)"
|
||||
>
|
||||
×
|
||||
</button>
|
||||
<!-- Estado vazio orientado -->
|
||||
<div v-else class="eli-tabela-modal-filtro__estado-vazio">
|
||||
<Filter :size="32" :stroke-width="1.5" />
|
||||
<span>Selecione um filtro abaixo para começar</span>
|
||||
</div>
|
||||
|
||||
<!-- Rodapé: adicionar filtro + fórmula lado a lado -->
|
||||
<div class="eli-tabela-modal-filtro__rodape-filtros">
|
||||
<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 eli-tabela-modal-filtro__botao--prim"
|
||||
@click="adicionar"
|
||||
:disabled="!chaveParaAdicionar"
|
||||
>
|
||||
<Plus :size="15" :stroke-width="2.5" />
|
||||
Adicionar
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Fórmula: só aparece com 2+ filtros (com 1 filtro, fórmula não faz sentido) -->
|
||||
<div v-if="linhas.length >= 2" class="eli-tabela-modal-filtro__formula-container">
|
||||
<label class="eli-tabela-modal-filtro__formula-label">Fórmula</label>
|
||||
<input
|
||||
v-model="formulaCustomizada"
|
||||
type="text"
|
||||
class="eli-tabela-modal-filtro__formula-input"
|
||||
:placeholder="'Ex: ' + formulaGerada"
|
||||
/>
|
||||
<span class="eli-tabela-modal-filtro__formula-ajuda" tabindex="0" aria-label="Ajuda sobre fórmulas">
|
||||
<CircleHelp :size="15" :stroke-width="2" />
|
||||
<div class="eli-tabela-modal-filtro__formula-tooltip" role="tooltip">
|
||||
<strong>Como usar a fórmula</strong>
|
||||
<p>Use os identificadores <code>{c1}</code>, <code>{c2}</code>, <code>{c3}</code>… correspondentes a cada filtro acima:</p>
|
||||
<ul>
|
||||
<li><code>E</code> — ambos devem ser verdadeiros</li>
|
||||
<li><code>OU</code> — pelo menos um deve ser verdadeiro</li>
|
||||
<li><code>NAO</code> — negação de uma condição</li>
|
||||
<li><code>( )</code> — agrupa condições</li>
|
||||
</ul>
|
||||
<p><strong>Exemplo:</strong></p>
|
||||
<code>{c1} OU ({c2} E NAO {c3})</code>
|
||||
<p class="eli-tabela-modal-filtro__formula-tooltip-nota">Deixe em branco para combinar todos com E.</p>
|
||||
</div>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</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>
|
||||
<footer class="eli-tabela-modal-filtro__footer">
|
||||
<button
|
||||
type="button"
|
||||
class="eli-tabela-modal-filtro__botao"
|
||||
@click="adicionar"
|
||||
:disabled="!chaveParaAdicionar"
|
||||
class="eli-tabela-modal-filtro__botao eli-tabela-modal-filtro__botao--perigo"
|
||||
:disabled="!linhas.length"
|
||||
@click="emitLimpar"
|
||||
>
|
||||
Adicionar
|
||||
Limpar
|
||||
</button>
|
||||
</div>
|
||||
<div class="eli-tabela-modal-filtro__footer-spacer"></div>
|
||||
<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>
|
||||
|
||||
<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>
|
||||
</Transition>
|
||||
</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
|
||||
}
|
||||
import { CircleHelp, Filter, Plus, Trash2, X } from "@lucide/vue"
|
||||
import { computed, defineComponent, type PropType } from "vue"
|
||||
import type { FiltroBase } from "./useFiltroAvancado"
|
||||
// biome-ignore lint/correctness/noUnusedImports: rotuloDoFiltro é usado no template Vue (invisível ao linter estático)
|
||||
import { rotuloDoFiltro, useFiltroAvancado } from "./useFiltroAvancado"
|
||||
import "./EliTabelaModalFiltroAvancado.css"
|
||||
|
||||
export default defineComponent({
|
||||
name: "EliTabelaModalFiltroAvancado",
|
||||
components: { CircleHelp, Filter, Plus, Trash2, X },
|
||||
props: {
|
||||
aberto: { type: Boolean, required: true },
|
||||
filtrosBase: {
|
||||
// biome-ignore lint/suspicious/noExplicitAny: generic component
|
||||
type: Array as PropType<Array<FiltroBase<any>>>,
|
||||
required: true,
|
||||
},
|
||||
filtrosBase: { type: Array as PropType<FiltroBase<string>[]>, required: true },
|
||||
modelo: {
|
||||
// biome-ignore lint/suspicious/noExplicitAny: dynamic model
|
||||
type: Array as PropType<Array<any>>,
|
||||
/** Linhas persistidas do pai — normalizadas internamente via normalizarModelo() */
|
||||
type: Array as PropType<Array<{ chave: string; valor: unknown }>>,
|
||||
required: true,
|
||||
},
|
||||
modeloFormula: { type: String, default: "" },
|
||||
},
|
||||
emits: {
|
||||
fechar: () => true,
|
||||
limpar: () => true,
|
||||
// biome-ignore lint/suspicious/noExplicitAny: dynamic lines
|
||||
salvar: (_linhas: any[]) => true,
|
||||
/** Dado serializável emitido pelo botão Aplicar */
|
||||
salvar: (_linhas: Array<{ chave: string; valor: unknown }>, _formula?: string) => 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)),
|
||||
)
|
||||
return useFiltroAvancado({
|
||||
aberto: computed(() => props.aberto),
|
||||
filtrosBase: computed(() => props.filtrosBase),
|
||||
modelo: computed(() => props.modelo),
|
||||
modeloFormula: computed(() => props.modeloFormula),
|
||||
emit,
|
||||
})
|
||||
|
||||
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>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue