diff --git a/.agent b/.agent index cdeaaaa..13ae9ca 100644 --- a/.agent +++ b/.agent @@ -40,6 +40,27 @@ Construir um Design System de componentes em **Vue 3** para reutilização em m --- +## Convenção atual de entradas (IMPORTANTE) + +O componente **`EliInput` foi removido**. O padrão atual é a família **`EliEntrada*`**: + +- `EliEntradaTexto` +- `EliEntradaNumero` +- `EliEntradaDataHora` + +E o contrato padrão para entradas é: +- prop `value` +- evento `update:value` +- prop obrigatória `opcoes` (contém `rotulo` e outras opções) + +Exemplo: + +```vue + +``` + +--- + ## Estrutura obrigatória do repositório - Cada componente deve possuir **sua própria pasta** em `src/componentes/` - Dentro de cada pasta do componente: @@ -162,6 +183,24 @@ Evitar comentários óbvios (“isso é um botão”). --- +## Convenção atual de EliTabela (IMPORTANTE) + +### Filtro avançado + +O filtro avançado da `EliTabela` é configurado via `tabela.filtroAvancado`. + +Regras: +- O **operador é travado na definição** (o usuário não escolhe operador) +- Cada filtro pode ser usado **no máximo 1 vez** +- UI: modal mostra **apenas os componentes de entrada** definidos no filtro +- Persistência: salva em `localStorage` apenas `{ coluna, valor }[]` por `tabela.nome` + +Se você for evoluir isso para backend: +- usar `parametrosConsulta.filtros` (`tipoFiltro[]`) no `tabela.consulta` +- manter compatibilidade com simulação local (quando necessário) + +--- + ## Publicação do pacote (npm) ### Regra de publicação (sem usar `package.json.files`) diff --git a/IA.md b/IA.md index 3d9d871..9992243 100644 --- a/IA.md +++ b/IA.md @@ -61,7 +61,15 @@ createApp(App) ### 2) Importação direta (quando não quiser plugin) ```ts -import { EliBotao, EliInput, EliBadge, EliCartao, EliDataHora } from "eli-vue"; +import { + EliBotao, + EliBadge, + EliCartao, + EliTabela, + EliEntradaTexto, + EliEntradaNumero, + EliEntradaDataHora, +} from "eli-vue"; ``` > Observação: ainda pode ser necessário importar o CSS do pacote: @@ -82,11 +90,18 @@ import "eli-vue/dist/eli-vue.css"; ``` -### Input com v-model +### Entradas (EliEntrada*) com v-model + +O `eli-vue` usa uma família de componentes `EliEntrada*` (em vez do antigo `EliInput`). + +#### Texto ```vue ``` -### Input de porcentagem +#### Texto com formato/máscara -Quando precisar de um campo numérico com sufixo `%`, use `type="porcentagem"`. +> Regra importante: o `value` emitido é **sempre o texto formatado** (igual ao que aparece no input). ```vue @@ -131,10 +150,10 @@ export default defineComponent({ ```vue ``` -### EliInput (porcentagem) - -Use `type="porcentagem"` quando precisar de um campo numérico com sufixo `%` embutido. +### EliEntradaNumero (exemplo) ```vue + + diff --git a/src/componentes/EliEntrada/EliEntradaNumero.vue b/src/componentes/EliEntrada/EliEntradaNumero.vue new file mode 100644 index 0000000..cd27780 --- /dev/null +++ b/src/componentes/EliEntrada/EliEntradaNumero.vue @@ -0,0 +1,236 @@ + + + + + diff --git a/src/componentes/EliEntrada/EliEntradaTexto.vue b/src/componentes/EliEntrada/EliEntradaTexto.vue new file mode 100644 index 0000000..9235593 --- /dev/null +++ b/src/componentes/EliEntrada/EliEntradaTexto.vue @@ -0,0 +1,101 @@ + + + + + diff --git a/src/componentes/EliEntrada/README.md b/src/componentes/EliEntrada/README.md new file mode 100644 index 0000000..2d2ee12 --- /dev/null +++ b/src/componentes/EliEntrada/README.md @@ -0,0 +1,176 @@ +# EliEntrada (Padrão de Entradas) + +Esta pasta define o **padrão EliEntrada**: um conjunto de componentes de entrada (inputs) com uma **API uniforme**. + +> TL;DR +> - Toda entrada recebe **`value`** (estado) e **`opcoes`** (configuração). +> - O padrão de uso é **`v-model:value`**. +> - Mantemos compatibilidade com Vue 2 via evento **`input`**. + +--- + +## Para humanos (uso no dia-a-dia) + +### Conceito + +Um componente **EliEntrada** recebe **duas propriedades**: + +- `value`: o valor atual do campo (entrada e saída) +- `opcoes`: um objeto que configura o componente (rótulo, placeholder e opções específicas do tipo) + +Essa padronização facilita: +- gerar formulários dinamicamente +- trocar tipos de entrada com o mínimo de refactor +- documentar e tipar de forma previsível + +### Tipos e contratos + +Os contratos ficam em: [`tiposEntradas.ts`](./tiposEntradas.ts) + +- `PadroesEntradas`: mapa de tipos suportados (ex.: `texto`, `numero`, `dataHora`) +- `TipoEntrada`: união das chaves do mapa (ex.: `"texto" | "numero" | "dataHora"`) + +### Componentes disponíveis + +#### 1) `EliEntradaTexto` + +**Value**: `string | null | undefined` + +**Opções** (além de `rotulo`/`placeholder`): +- `limiteCaracteres?: number` + +Exemplo: + +```vue + + + +``` + +--- + +#### 2) `EliEntradaNumero` + +**Value**: `number | null | undefined` + +**Opções**: +- `precisao?: number` + - `1` => inteiro + - `0.1` => 1 casa decimal + - `0.01` => 2 casas decimais +- `prefixo?: string` (ex.: `"R$"`) +- `sufixo?: string` (ex.: `"kg"`) + +Comportamento: +- Quando `precisao < 1` o componente entra em modo **fixed-point**: você digita números continuamente e ele insere a vírgula automaticamente. +- O que é exibido sempre corresponde ao `value` emitido. + +Exemplos: + +```vue + + + +``` + +--- + +#### 3) `EliEntradaDataHora` + +**Value**: `string | null | undefined` (ISO 8601 com offset ou `Z`) + +**Opções**: +- `modo?: "data" | "dataHora"` (default: `dataHora`) +- `min?: string` (ISO) +- `max?: string` (ISO) +- `limpavel?: boolean` +- `erro?: boolean` +- `mensagensErro?: string | string[]` +- `dica?: string` +- `dicaPersistente?: boolean` +- `densidade?: CampoDensidade` +- `variante?: CampoVariante` + +Importante: +- O input nativo `datetime-local` não carrega timezone. +- O componente converte ISO (Z/offset) para **local** para exibir. +- Ao alterar, emite ISO 8601 com o **offset local**. + +Exemplo: + +```vue + +``` + +### Compatibilidade Vue 2 / Vue 3 + +Padrão recomendado (Vue 3): +- `v-model:value` + +Compat Vue 2: +- todos os EliEntradas também emitem `input`. +- isso permite consumir com o padrão `value + input` quando necessário. + +### Playground + +- Entradas: `src/playground/entradas.playground.vue` +- Data/hora: `src/playground/data_hora.playground.vue` + +--- + +## Para IA (contratos, invariantes e padrões de evolução) + +### Contratos (não quebrar) + +1) **Todo EliEntrada tem**: + - prop `value` + - prop `opcoes` + - evento `update:value` + +2) **Compatibilidade**: + - emitir `input` (compat Vue 2) é obrigatório + +3) **Tipagem**: + - `PadroesEntradas` é a fonte única do contrato (value/opcoes) + - `TipoEntrada = keyof PadroesEntradas` + +4) **Sanitização/Normalização**: + - `EliEntradaNumero` deve bloquear caracteres inválidos e manter display coerente com `value` + - `EliEntradaDataHora` deve receber/emitir ISO e converter para local apenas para exibição + +### Como adicionar uma nova entrada (checklist) + +1) Adicionar chave em `PadroesEntradas` em `tiposEntradas.ts` +2) Criar `EliEntradaX.vue` seguindo o padrão: + - `value` + `opcoes` + - emite `update:value`, `input`, `change` +3) Exportar no `src/componentes/EliEntrada/index.ts` +4) Registrar no `src/componentes/EliEntrada/registryEliEntradas.ts` +5) Criar/atualizar playground (`src/playground/*.playground.vue`) +6) Validar `pnpm -s run build:types` e `pnpm -s run build` + +### Padrões de mudança (refactors seguros) + +- Se precisar mudar o contrato, faça **migração incremental**: + - manter props/eventos antigos como fallback temporário + - atualizar playground e exemplos + - rodar `build:types` para garantir geração de `.d.ts` diff --git a/src/componentes/EliEntrada/index.ts b/src/componentes/EliEntrada/index.ts new file mode 100644 index 0000000..55ac506 --- /dev/null +++ b/src/componentes/EliEntrada/index.ts @@ -0,0 +1,6 @@ +import EliEntradaTexto from "./EliEntradaTexto.vue"; +import EliEntradaNumero from "./EliEntradaNumero.vue"; +import EliEntradaDataHora from "./EliEntradaDataHora.vue"; + +export { EliEntradaTexto, EliEntradaNumero, EliEntradaDataHora }; +export type { PadroesEntradas, TipoEntrada } from "./tiposEntradas"; diff --git a/src/componentes/EliEntrada/registryEliEntradas.ts b/src/componentes/EliEntrada/registryEliEntradas.ts new file mode 100644 index 0000000..99e21c6 --- /dev/null +++ b/src/componentes/EliEntrada/registryEliEntradas.ts @@ -0,0 +1,13 @@ +import type { Component } from "vue"; + +import EliEntradaTexto from "./EliEntradaTexto.vue"; +import EliEntradaNumero from "./EliEntradaNumero.vue"; +import EliEntradaDataHora from "./EliEntradaDataHora.vue"; + +import type { TipoEntrada } from "./tiposEntradas"; + +export const registryTabelaCelulas = { + texto: EliEntradaTexto, + numero: EliEntradaNumero, + dataHora: EliEntradaDataHora, +} as const satisfies Record; diff --git a/src/componentes/EliEntrada/tiposEntradas.ts b/src/componentes/EliEntrada/tiposEntradas.ts new file mode 100644 index 0000000..5dd16c6 --- /dev/null +++ b/src/componentes/EliEntrada/tiposEntradas.ts @@ -0,0 +1,132 @@ +/** + * Tipos base para componentes de entrada (EliEntrada*) + * + * Objetivo: + * - Padronizar o shape de dados de todos os componentes de entrada. + * - Cada entrada possui sempre: + * 1) `value`: o valor atual (tipado) + * 2) `opcoes`: configuração do componente (rótulo, placeholder e extras por tipo) + * + * Como usar: + * - `PadroesEntradas[tipo]` retorna a tipagem completa (value + opcoes) daquele tipo. + * - `TipoEntrada` é a união com todos os tipos suportados (ex.: "texto" | "numero"). + */ + +/** + * Contrato padrão de uma entrada. + * + * @typeParam T - tipo do `value` (ex.: string | null | undefined) + * @typeParam Mais - campos adicionais dentro de `opcoes`, específicos do tipo de entrada + */ +export type tipoPadraoEntrada = {}> = { + /** Valor atual do campo (pode aceitar null/undefined quando aplicável) */ + value: T + + /** Configurações do componente (visuais + regras simples do tipo) */ + opcoes: { + /** Rótulo exibido ao usuário */ + rotulo: string + + /** Texto de ajuda dentro do input quando vazio */ + placeholder?: string + } & Mais +} + +/** + * Mapa de tipos de entrada suportados e suas configurações específicas. + * + * Observação importante: + * - As chaves deste objeto (ex.: "texto", "numero") viram o tipo `TipoEntrada`. + * - Cada item define: + * - `value`: tipo do valor + * - `opcoes`: opções comuns + extras específicas + */ +export type PadroesEntradas = { + texto: tipoPadraoEntrada< + string | null | undefined, + { + /** Limite máximo de caracteres permitidos (se definido) */ + limiteCaracteres?: number + + /** + * Formato/máscara aplicada ao texto. + * Obs: o `value` SEMPRE será o texto formatado (o que aparece no input). + */ + formato?: "texto" | "email" | "url" | "telefone" | "cpfCnpj" | "cep" + } + > + + numero: tipoPadraoEntrada< + number | null | undefined, + { + + + /** Unidade de medida (ex.: "kg", "m³") */ + sufixo?: string + + /** Moéda (ex.: "R$") */ + prefixo?: string + + /** + * Passo/precisão do valor numérico. + * - 1 => somente inteiros + * - 0.1 => 1 casa decimal + * - 0.01 => 2 casas decimais + * + * Dica: este conceito corresponde ao atributo HTML `step`. + */ + precisao?: number + } + > + + dataHora: tipoPadraoEntrada< + string | null | undefined, + { + /** Define o tipo de entrada. - `dataHora`: datetime-local - `data`: date */ + modo?: "data" | "dataHora" + + /** Se true, mostra ícone para limpar o valor (Vuetify clearable). */ + limpavel?: boolean + + /** Estado de erro (visual). */ + erro?: boolean + + /** Mensagens de erro. */ + mensagensErro?: string | string[] + + /** Texto de apoio. */ + dica?: string + + /** Mantém a dica sempre visível. */ + dicaPersistente?: boolean + + /** Valor mínimo permitido (ISO 8601 - offset ou Z). */ + min?: string + + /** Valor máximo permitido (ISO 8601 - offset ou Z). */ + max?: string + + /** Densidade do campo (Vuetify). */ + densidade?: import("../../tipos").CampoDensidade + + /** Variante do v-text-field (Vuetify). */ + variante?: import("../../tipos").CampoVariante + } + > +} + +/** + * União dos tipos de entrada suportados. + * Ex.: "texto" | "numero" + */ +export type TipoEntrada = keyof PadroesEntradas + + + + +export type PadraoComponenteEntrada = + readonly [T, PadroesEntradas[T]['opcoes']] + +export type ComponenteEntrada = { + [K in TipoEntrada]: PadraoComponenteEntrada +}[TipoEntrada] \ No newline at end of file diff --git a/src/componentes/EliEntrada/utils/cep.ts b/src/componentes/EliEntrada/utils/cep.ts new file mode 100644 index 0000000..9eb4cb6 --- /dev/null +++ b/src/componentes/EliEntrada/utils/cep.ts @@ -0,0 +1,10 @@ +function somenteNumeros(valor: string) { + return valor.replace(/\D+/g, ""); +} + +/** Formata CEP no padrão 00000-000 */ +export function formatarCep(valor: string) { + const digitos = somenteNumeros(valor); + if (!digitos) return ""; + return digitos.replace(/^(\d{5})(\d)/, "$1-$2").slice(0, 9); +} diff --git a/src/componentes/campo/utils/cpfCnpj.ts b/src/componentes/EliEntrada/utils/cpfCnpj.ts similarity index 100% rename from src/componentes/campo/utils/cpfCnpj.ts rename to src/componentes/EliEntrada/utils/cpfCnpj.ts diff --git a/src/componentes/campo/utils/telefone.ts b/src/componentes/EliEntrada/utils/telefone.ts similarity index 96% rename from src/componentes/campo/utils/telefone.ts rename to src/componentes/EliEntrada/utils/telefone.ts index 66fbd46..dcd7d04 100644 --- a/src/componentes/campo/utils/telefone.ts +++ b/src/componentes/EliEntrada/utils/telefone.ts @@ -1,5 +1,3 @@ -// utils/telefone.ts - /** * Remove tudo que não é número */ diff --git a/src/components/eli/EliTabela/EliTabela.css b/src/componentes/EliTabela/EliTabela.css similarity index 95% rename from src/components/eli/EliTabela/EliTabela.css rename to src/componentes/EliTabela/EliTabela.css index 9d2563a..cbc5f4a 100644 --- a/src/components/eli/EliTabela/EliTabela.css +++ b/src/componentes/EliTabela/EliTabela.css @@ -95,26 +95,7 @@ background: rgba(0, 0, 0, 0.03); } -.eli-tabela__celula--esquerda { - text-align: left; -} -.eli-tabela__celula--centro { - text-align: center; -} - -.eli-tabela__celula--direita { - text-align: right; -} - -.eli-tabela__celula-conteudo { - display: inline-block; - max-width: 100%; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; - vertical-align: top; -} .eli-tabela--erro { border: 1px solid rgba(220, 53, 69, 0.35); diff --git a/src/components/eli/EliTabela/EliTabela.vue b/src/componentes/EliTabela/EliTabela.vue similarity index 72% rename from src/components/eli/EliTabela/EliTabela.vue rename to src/componentes/EliTabela/EliTabela.vue index f12f586..a3c37f4 100644 --- a/src/components/eli/EliTabela/EliTabela.vue +++ b/src/componentes/EliTabela/EliTabela.vue @@ -13,20 +13,32 @@ + + (null); const direcaoOrdenacao = ref<"asc" | "desc">("asc"); + /** Filtro avançado (config + estado modal) */ + const modalFiltroAberto = ref(false); + type LinhaFiltroUI = { + coluna: keyof T; + valor: any; + }; + + const filtrosUi = ref>>(carregarFiltroAvancado(props.tabela.nome) as any); + + function abrirModalFiltro() { + modalFiltroAberto.value = true; + } + + function fecharModalFiltro() { + modalFiltroAberto.value = false; + } + + function limparFiltrosAvancados() { + filtrosUi.value = []; + limparFiltroAvancado(props.tabela.nome); + modalFiltroAberto.value = false; + if (paginaAtual.value !== 1) paginaAtual.value = 1; + } + + function salvarFiltrosAvancados(novo: any[]) { + filtrosUi.value = (novo ?? []) as any; + salvarFiltroAvancado(props.tabela.nome, (novo ?? []) as any); + modalFiltroAberto.value = false; + if (paginaAtual.value !== 1) paginaAtual.value = 1; + } + + const filtrosAvancadosAtivos = computed(() => { + // Operador vem travado na definição (`tabela.filtroAvancado`). + const base = (props.tabela.filtroAvancado ?? []) as Array<{ coluna: string; operador: any }>; + + return (filtrosUi.value ?? []) + .filter((f) => f && f.coluna !== undefined) + .map((f) => { + const b = base.find((x) => String(x.coluna) === String(f.coluna)); + if (!b) return null; + + return { + coluna: String(b.coluna), + operador: b.operador as any, + valor: (f as any).valor, + } as tipoFiltro; + }) + .filter(Boolean) as tipoFiltro[]; + }); + /** Alias reativo da prop tabela */ const tabela = computed(() => props.tabela); @@ -171,11 +243,20 @@ export default defineComponent({ const colunasInvisiveisEfetivas = computed(() => { const colunas = props.tabela.colunas as Array>; - const invisiveisSet = new Set(configColunas.value.invisiveis ?? []); + + const configTemDados = + (configColunas.value.visiveis?.length ?? 0) > 0 || + (configColunas.value.invisiveis?.length ?? 0) > 0; + + const invisiveisBaseRotulos = configTemDados + ? configColunas.value.invisiveis ?? [] + : colunas.filter((c) => c.visivel === false).map((c) => c.rotulo); + + const invisiveisSet = new Set(invisiveisBaseRotulos); const base = colunas.filter((c) => invisiveisSet.has(c.rotulo)); - // ordenação: usa a lista salva de invisíveis (se existir), senão segue ordem original - const ordemSalva = configColunas.value.invisiveis ?? []; + // ordenação: usa a lista (salva ou derivada do default) e adiciona novas ao final + const ordemSalva = invisiveisBaseRotulos; const mapa = new Map>(); for (const c of base) { if (!mapa.has(c.rotulo)) mapa.set(c.rotulo, c); @@ -198,14 +279,25 @@ export default defineComponent({ const colunasEfetivas = computed(() => { const colunas = props.tabela.colunas; const todosRotulos = rotulosColunas.value; - const invisiveisSet = new Set(configColunas.value.invisiveis ?? []); - // default: todas visiveis; so some se estiver explicitamente em invisiveis + const configTemDados = + (configColunas.value.visiveis?.length ?? 0) > 0 || + (configColunas.value.invisiveis?.length ?? 0) > 0; + + const invisiveisBaseRotulos = configTemDados + ? configColunas.value.invisiveis ?? [] + : (props.tabela.colunas as Array>) + .filter((c) => c.visivel === false) + .map((c) => c.rotulo); + + const invisiveisSet = new Set(invisiveisBaseRotulos); + + // base visiveis: remove invisiveis const visiveisBaseRotulos = todosRotulos.filter((r) => !invisiveisSet.has(r)); const visiveisSet = new Set(visiveisBaseRotulos); // aplica ordem salva; novas (sem definicao) entram no fim, respeitando ordem original - const ordemSalva = configColunas.value.visiveis ?? []; + const ordemSalva = configTemDados ? configColunas.value.visiveis ?? [] : []; const ordemFinal: string[] = []; for (const r of ordemSalva) { @@ -259,20 +351,91 @@ export default defineComponent({ return 10; }); - /** Total de páginas calculado com base na quantidade */ + function aplicarFiltroTexto(linhasIn: unknown[]) { + const q = (valorBusca.value ?? "").trim().toLowerCase(); + if (!q) return linhasIn; + // filtro simples: stringifica o objeto + return linhasIn.filter((l) => JSON.stringify(l).toLowerCase().includes(q)); + } + + function compararOperador(operador: string, valorLinha: any, valorFiltro: any): boolean { + switch (operador) { + case "=": + return valorLinha == valorFiltro; + case "!=": + return valorLinha != valorFiltro; + case ">": + return Number(valorLinha) > Number(valorFiltro); + case ">=": + return Number(valorLinha) >= Number(valorFiltro); + case "<": + return Number(valorLinha) < Number(valorFiltro); + case "<=": + return Number(valorLinha) <= Number(valorFiltro); + case "like": { + const a = String(valorLinha ?? "").toLowerCase(); + const b = String(valorFiltro ?? "").toLowerCase(); + return a.includes(b); + } + case "in": { + // aceita "a,b,c" ou array + const arr = Array.isArray(valorFiltro) + ? valorFiltro + : String(valorFiltro ?? "") + .split(",") + .map((s) => s.trim()) + .filter(Boolean); + return arr.includes(String(valorLinha)); + } + case "isNull": + return valorLinha === null || valorLinha === undefined || valorLinha === ""; + default: + return true; + } + } + + function aplicarFiltroAvancado(linhasIn: unknown[]) { + const filtros = filtrosAvancadosAtivos.value; + if (!filtros.length) return linhasIn; + + return linhasIn.filter((l: any) => { + return filtros.every((f) => { + const vLinha = l?.[f.coluna as any]; + return compararOperador(String(f.operador), vLinha, (f as any).valor); + }); + }); + } + + const linhasFiltradas = computed(() => { + const base = linhas.value ?? []; + return aplicarFiltroAvancado(aplicarFiltroTexto(base)); + }); + + /** Quantidade agora segue a filtragem local */ + const quantidadeFiltrada = computed(() => linhasFiltradas.value.length); + + /** Total de páginas calculado com base no filtrado */ const totalPaginas = computed(() => { const limite = registrosPorConsulta.value; if (!limite || limite <= 0) return 1; - const total = quantidade.value; + const total = quantidadeFiltrada.value; if (!total) return 1; return Math.max(1, Math.ceil(total / limite)); }); + const linhasPaginadas = computed(() => { + const limite = Math.max(1, registrosPorConsulta.value); + const offset = (paginaAtual.value - 1) * limite; + return linhasFiltradas.value.slice(offset, offset + limite); + }); + /** Indica se existem ações por linha */ const temAcoes = computed(() => (props.tabela.acoesLinha ?? []).length > 0); + const exibirFiltroAvancado = computed(() => (props.tabela.filtroAvancado ?? []).length > 0); + /** Sequencial para evitar race conditions entre consultas */ let carregamentoSequencial = 0; @@ -431,8 +594,10 @@ export default defineComponent({ menuAberto.value = null; linhasExpandidas.value = {}; + // Em modo simulação (filtro local), sempre buscamos a lista completa. + // A paginação é aplicada APÓS a filtragem. const limite = Math.max(1, registrosPorConsulta.value); - const offset = (paginaAtual.value - 1) * limite; + const offset = 0; const parametrosConsulta: { coluna_ordem?: never; @@ -442,12 +607,10 @@ export default defineComponent({ texto_busca?: string; } = { offSet: offset, - limit: limite, + limit: 999999, }; - if (valorBusca.value) { - parametrosConsulta.texto_busca = valorBusca.value; - } + // texto_busca ficará somente para filtragem local. if (colunaOrdenacao.value) { parametrosConsulta.coluna_ordem = colunaOrdenacao.value as never; @@ -468,12 +631,12 @@ export default defineComponent({ } const valores = res.valor?.valores ?? []; - const total = res.valor?.quantidade ?? valores.length; + const total = valores.length; linhas.value = valores; quantidade.value = total; - const totalPaginasRecalculado = Math.max(1, Math.ceil((total || 0) / limite)); + const totalPaginasRecalculado = Math.max(1, Math.ceil((quantidadeFiltrada.value || 0) / limite)); if (paginaAtual.value > totalPaginasRecalculado) { paginaAtual.value = totalPaginasRecalculado; return; @@ -557,7 +720,10 @@ export default defineComponent({ /** Watch: mudança de página dispara nova consulta */ watch(paginaAtual, (nova, antiga) => { - if (nova !== antiga) void carregar(); + // paginação local não precisa recarregar + if (nova !== antiga) { + // noop + } }); /** Watch: troca de configuração reseta estados e recarrega */ @@ -569,7 +735,9 @@ export default defineComponent({ direcaoOrdenacao.value = "asc"; valorBusca.value = ""; modalColunasAberto.value = false; + modalFiltroAberto.value = false; configColunas.value = carregarConfigColunas(props.tabela.nome); + filtrosUi.value = carregarFiltroAvancado(props.tabela.nome) as any; linhasExpandidas.value = {}; if (paginaAtual.value !== 1) { paginaAtual.value = 1; @@ -605,6 +773,8 @@ export default defineComponent({ carregando, erro, linhas, + linhasPaginadas, + quantidadeFiltrada, quantidade, menuAberto, valorBusca, @@ -615,6 +785,7 @@ export default defineComponent({ // computed exibirBusca, + exibirFiltroAvancado, acoesCabecalho, temAcoesCabecalho, temAcoes, @@ -628,9 +799,16 @@ export default defineComponent({ linhasExpandidas, abrirModalColunas, + abrirModalFiltro, fecharModalColunas, salvarModalColunas, + modalFiltroAberto, + filtrosUi, + salvarFiltrosAvancados, + limparFiltrosAvancados, + fecharModalFiltro, + alternarLinhaExpandida, // actions diff --git a/src/components/eli/EliTabela/EliTabelaBody.vue b/src/componentes/EliTabela/EliTabelaBody.vue similarity index 72% rename from src/components/eli/EliTabela/EliTabelaBody.vue rename to src/componentes/EliTabela/EliTabelaBody.vue index adadd9f..133bd27 100644 --- a/src/components/eli/EliTabela/EliTabelaBody.vue +++ b/src/componentes/EliTabela/EliTabelaBody.vue @@ -29,22 +29,8 @@ v-for="(coluna, j) in colunas" :key="`td-${i}-${j}`" class="eli-tabela__td" - :class="[ - coluna.acao ? 'eli-tabela__td--clicavel' : undefined, - obterClasseAlinhamento(coluna.alinhamento), - ]" - @click="coluna.acao ? () => coluna.acao?.() : undefined" > - - - - - +
@@ -145,40 +131,9 @@ export default defineComponent({ }, }, setup() { - function obterClasseAlinhamento(alinhamento?: string) { - if (alinhamento === "direita") return "eli-tabela__celula--direita"; - if (alinhamento === "centro") return "eli-tabela__celula--centro"; - return "eli-tabela__celula--esquerda"; - } - - function obterMaxWidth(largura?: number | string) { - if (largura === undefined || largura === null) return undefined; - return typeof largura === "number" ? `${largura}px` : String(largura); - } - - function obterTooltipCelula(celula: unknown) { - if (!Array.isArray(celula)) return undefined; - - const tipo = celula[0]; - const dados = celula[1] as any; - - if (tipo === "textoSimples") { - return typeof dados?.texto === "string" ? dados.texto : undefined; - } - - if (tipo === "numero") { - return typeof dados?.numero === "number" ? String(dados.numero) : undefined; - } - - return undefined; - } - return { ChevronRight, ChevronDown, - obterClasseAlinhamento, - obterMaxWidth, - obterTooltipCelula, }; }, }); diff --git a/src/components/eli/EliTabela/EliTabelaCabecalho.vue b/src/componentes/EliTabela/EliTabelaCabecalho.vue similarity index 81% rename from src/components/eli/EliTabela/EliTabelaCabecalho.vue rename to src/componentes/EliTabela/EliTabelaCabecalho.vue index c3a4865..7a8b66e 100644 --- a/src/components/eli/EliTabela/EliTabelaCabecalho.vue +++ b/src/componentes/EliTabela/EliTabelaCabecalho.vue @@ -10,6 +10,15 @@ > Colunas + + @@ -53,6 +62,11 @@ export default defineComponent({ required: false, default: true, }, + exibirBotaoFiltroAvancado: { + type: Boolean, + required: false, + default: false, + }, valorBusca: { type: String, required: true, @@ -76,6 +90,9 @@ export default defineComponent({ colunas() { return true; }, + filtroAvancado() { + return true; + }, }, setup(props, { emit }) { const temAcoesCabecalho = computed(() => props.acoesCabecalho.length > 0); @@ -88,7 +105,11 @@ export default defineComponent({ emit("colunas"); } - return { temAcoesCabecalho, emitBuscar, emitColunas }; + function emitFiltroAvancado() { + emit("filtroAvancado"); + } + + return { temAcoesCabecalho, emitBuscar, emitColunas, emitFiltroAvancado }; }, }); diff --git a/src/components/eli/EliTabela/EliTabelaCaixaDeBusca.vue b/src/componentes/EliTabela/EliTabelaCaixaDeBusca.vue similarity index 100% rename from src/components/eli/EliTabela/EliTabelaCaixaDeBusca.vue rename to src/componentes/EliTabela/EliTabelaCaixaDeBusca.vue diff --git a/src/components/eli/EliTabela/EliTabelaDebug.vue b/src/componentes/EliTabela/EliTabelaDebug.vue similarity index 100% rename from src/components/eli/EliTabela/EliTabelaDebug.vue rename to src/componentes/EliTabela/EliTabelaDebug.vue diff --git a/src/components/eli/EliTabela/EliTabelaDetalhesLinha.vue b/src/componentes/EliTabela/EliTabelaDetalhesLinha.vue similarity index 100% rename from src/components/eli/EliTabela/EliTabelaDetalhesLinha.vue rename to src/componentes/EliTabela/EliTabelaDetalhesLinha.vue diff --git a/src/components/eli/EliTabela/EliTabelaEstados.vue b/src/componentes/EliTabela/EliTabelaEstados.vue similarity index 100% rename from src/components/eli/EliTabela/EliTabelaEstados.vue rename to src/componentes/EliTabela/EliTabelaEstados.vue diff --git a/src/components/eli/EliTabela/EliTabelaHead.vue b/src/componentes/EliTabela/EliTabelaHead.vue similarity index 85% rename from src/components/eli/EliTabela/EliTabelaHead.vue rename to src/componentes/EliTabela/EliTabelaHead.vue index 0cd741b..ed314a6 100644 --- a/src/components/eli/EliTabela/EliTabelaHead.vue +++ b/src/componentes/EliTabela/EliTabelaHead.vue @@ -7,10 +7,7 @@ v-for="(coluna, idx) in colunas" :key="`th-${idx}`" class="eli-tabela__th" - :class="[ - isOrdenavel(coluna) ? 'eli-tabela__th--ordenavel' : undefined, - obterClasseAlinhamento(coluna.alinhamento), - ]" + :class="[isOrdenavel(coluna) ? 'eli-tabela__th--ordenavel' : undefined]" scope="col" >