Atualização floresta

This commit is contained in:
Luiz Silva 2026-05-31 11:27:51 -03:00
parent 0886fd5c3e
commit 2d4d8f8929
103 changed files with 19266 additions and 14160 deletions

View file

@ -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>