Atualização floresta
This commit is contained in:
parent
0886fd5c3e
commit
2d4d8f8929
103 changed files with 19266 additions and 14160 deletions
|
|
@ -34,12 +34,13 @@ baixar "$BASE/workflows/aprimorar-IA.global.md" "$DIR/workflows
|
|||
baixar "$BASE/workflows/adicionar-ajuda.global.md" "$DIR/workflows/adicionar-ajuda.global.md"
|
||||
baixar "$BASE/workflows/atualizar-agente.global.md" "$DIR/workflows/atualizar-agente.global.md"
|
||||
baixar "$BASE/workflows/criar-fluxograma.global.md" "$DIR/workflows/criar-fluxograma.global.md"
|
||||
baixar "$BASE/workflows/documentar-codigo.global.md" "$DIR/workflows/documentar-codigo.global.md"
|
||||
baixar "$BASE/workflows/otimizar-codigos.global.md" "$DIR/workflows/otimizar-codigos.global.md"
|
||||
baixar "$BASE/workflows/comercial.global.md" "$DIR/workflows/comercial.global.md"
|
||||
baixar "$BASE/workflows/marketing.global.md" "$DIR/workflows/marketing.global.md"
|
||||
baixar "$BASE/workflows/orcamento.global.md" "$DIR/workflows/orcamento.global.md"
|
||||
baixar "$BASE/workflows/reels.global.md" "$DIR/workflows/reels.global.md"
|
||||
baixar "$BASE/workflows/documentar-regras-negocio.global.md" "$DIR/workflows/documentar-regras-negocio.global.md"
|
||||
baixar "$BASE/workflows/ux.global.md" "$DIR/workflows/ux.global.md"
|
||||
|
||||
|
||||
# skills
|
||||
|
|
|
|||
60
.agent/rules/vue-limite-linhas.global.md
Normal file
60
.agent/rules/vue-limite-linhas.global.md
Normal file
|
|
@ -0,0 +1,60 @@
|
|||
# Regra: Limite de Linhas em Arquivos Vue
|
||||
|
||||
## 🎯 Regra
|
||||
|
||||
**Nenhum arquivo `*.vue` deve ultrapassar 250 linhas.**
|
||||
|
||||
---
|
||||
|
||||
## Motivação
|
||||
|
||||
Arquivos Vue longos acumulam múltiplas responsabilidades:
|
||||
|
||||
- template, lógica e estilo no mesmo arquivo
|
||||
- componentes internos não reutilizáveis
|
||||
- dificuldade de teste e manutenção
|
||||
|
||||
O limite de 250 linhas é uma fronteira de manutenibilidade, não apenas de estética.
|
||||
|
||||
---
|
||||
|
||||
## Aplicação automática
|
||||
|
||||
O script `scripts/check-vue-linhas.sh` valida este limite em todo `pnpm run build`.
|
||||
Arquivos legados em processo de refatoração estão listados como **isentos temporários** dentro do script.
|
||||
|
||||
---
|
||||
|
||||
## Obrigações do agente
|
||||
|
||||
### Antes de criar um novo arquivo Vue
|
||||
|
||||
- Estimar o tamanho do componente
|
||||
- Se a estimativa ultrapassar 250 linhas, **quebrar em subcomponentes imediatamente**
|
||||
|
||||
### Ao editar um arquivo Vue existente
|
||||
|
||||
- Verificar o total de linhas atual antes de adicionar código
|
||||
- Se a edição levar o arquivo acima de 250 linhas, propor extração de subcomponente
|
||||
|
||||
### Ao identificar um arquivo isento
|
||||
|
||||
- Sinalizar ao operador que o arquivo é legado e deve ser refatorado
|
||||
- Não adicionar mais linhas a arquivos isentos sem refatorar algo equivalente
|
||||
|
||||
---
|
||||
|
||||
## Estratégias de extração obrigatórias
|
||||
|
||||
Quando um arquivo se aproximar do limite:
|
||||
|
||||
| Situação | Ação |
|
||||
|---|---|
|
||||
| Template longo | Extrair seções como `<NomeSecao />` próprio componente |
|
||||
| CSS extenso | Mover para arquivo `.css` co-localizado e importar |
|
||||
| Setup() > 100 linhas | Extrair lógica para composable `use*.ts` co-localizado |
|
||||
| Modal/Dialog embutido | Sempre em componente separado (`*Modal.vue`) |
|
||||
|
||||
---
|
||||
|
||||
> Não existem isentos. Todos os arquivos Vue da biblioteca respeitam o limite de 250 linhas.
|
||||
|
|
@ -1,58 +0,0 @@
|
|||
---
|
||||
description: Workflow para criação e padronização de documentação em código (JSDoc, comentários e tipagem)
|
||||
---
|
||||
|
||||
# Workflow: Documentar Código
|
||||
|
||||
## 🎯 Objetivo
|
||||
Estabelecer um padrão global e unificado para a redação de documentação em código (comentários, `JSDoc` e explicações arquiteturais) em todas as aplicações, garantindo clareza sem prolixidade e respeitando a política de tipagem explícita.
|
||||
|
||||
---
|
||||
|
||||
## 1️⃣ Princípios Básicos de Documentação
|
||||
- **Por que, não o Quê**: Documente o raciocínio por trás da decisão arquitetural ou a regra de negócio que forçou a estratégia, não o mecanismo óbvio que o TypeScript nativamente expressaria.
|
||||
- **Nomenclatura Limpa**: Confie na auto-documentação de nomes de variáveis expressivos baseados no domínio antes de escrever um comentário de texto livre.
|
||||
|
||||
## 2️⃣ Funções Compartilhadas e JSDoc (Obrigatória)
|
||||
Toda função central ou de uso compartilhado, independente da aplicação (frontend, backend, mobile ou bibliotecas comuns), deve portar cabeçalho em `JSDoc`.
|
||||
|
||||
### Padrão para JSDoc
|
||||
```typescript
|
||||
/**
|
||||
* Busca os detalhes de uma entidade baseando-se no ID fornecido e calcula privilégios aplicáveis.
|
||||
* Não utilizar em fluxos de processamento em lote para evitar sobrecarga de requisições ou banco.
|
||||
*
|
||||
* @param tenantId - Identificador único do cliente/tenant no sistema
|
||||
* @param entityId - ID exclusivo identificando o registro na base de dados
|
||||
* @returns Retorna o objeto consolidado com dados ou `null` caso não seja encontrado
|
||||
*/
|
||||
export const obterDetalhesEntidade = async (tenantId: string, entityId: string) => { ... }
|
||||
```
|
||||
|
||||
## 3️⃣ Tratamento de Exceções e Casts Temporários
|
||||
Devido à política `TypeScript sem any`, não utilizamos conversores obscuros imperativamente.
|
||||
Porém, se uma coerção agressiva (`as unknown as X`) demonstrar-se temporariamente necessária por divergência ou limitação de SDK terceiro, **o comentário justificando a anomalia é obrigatório**.
|
||||
|
||||
```typescript
|
||||
// Necessário cast duplo pois a biblioteca externa não provê tipagem genérica adequada na versão atual
|
||||
const dados = payload.raw as unknown as RespostaFormatada
|
||||
```
|
||||
|
||||
## 4️⃣ Atribuição de TODOs
|
||||
Toda refatoração, debito técnico visualizado ou dúvida de negócio deixada aberta em código de longo prazo **não prescreve** e deve ser acompanhada do formato `// TODO`:
|
||||
- **Tipagem**: `// TODO de tipagem: Criar interface estrita para o retorno da API de listagem (Removendo Partial)`
|
||||
- **Negócio**: `// TODO de regra: Validar com a equipe de Produto se a nova taxa adicional será inserida para clientes antigos.`
|
||||
|
||||
## 5️⃣ Validação do Fluxo
|
||||
Sempre ao propor documentação de um arquivo novo ou refatorado, valide via checklist:
|
||||
* [ ] A documentação está aderente sem estar redundante à tipagem TS?
|
||||
* [ ] A formatação de parágrafos/espaçamento quebra alguma regra no formatador?
|
||||
* [ ] A sintaxe suporta autocompletion na sua camada dependente (`@params` estão exatos)?
|
||||
|
||||
---
|
||||
|
||||
## Referências
|
||||
|
||||
- Rules: `.agent/rules/typescript-sem-any.global.md` (política de tipagem que influencia a documentação)
|
||||
- Skill: `.agent/skills/documentar-decisao-tecnica/SKILL.md` (para decisões arquiteturais que exigem ADR além de JSDoc)
|
||||
|
||||
221
.agent/workflows/otimizar-codigos.global.md
Normal file
221
.agent/workflows/otimizar-codigos.global.md
Normal file
|
|
@ -0,0 +1,221 @@
|
|||
---
|
||||
description: Otimizar código e documentar padrões de qualidade (estrutura, limites, JSDoc e tipagem)
|
||||
---
|
||||
|
||||
|
||||
|
||||
# Workflow: Qualidade e Documentação de Código
|
||||
|
||||
## 🎯 Objetivo
|
||||
|
||||
Unificar as regras de **estrutura de arquivos**, **otimização** e **documentação** em um único fluxo de referência para o projeto `eli-vue`.
|
||||
|
||||
---
|
||||
|
||||
## 1️⃣ Limite de Linhas por Arquivo
|
||||
|
||||
Nenhum arquivo `*.vue`, `*.ts` ou `*.tsx` pode ultrapassar **250 linhas**.
|
||||
|
||||
Essa regra existe para facilitar a leitura humana, a revisão de código e a manutenção a longo prazo.
|
||||
|
||||
> O script `scripts/check-vue-linhas.sh` valida automaticamente os arquivos `.vue` no `prebuild`.
|
||||
|
||||
**Estratégias quando o limite se aproxima:**
|
||||
|
||||
| Situação | Ação |
|
||||
|---|---|
|
||||
| Template longo | Extrair seções como subcomponentes |
|
||||
| CSS extenso | Mover para `<nome>.css` co-localizado |
|
||||
| `setup()` extenso | Extrair subcomponentes, CSS ou Dispatch Table |
|
||||
| Modal/Dialog embutido | Sempre em componente separado (`*Modal.vue`) |
|
||||
| Handlers e dispatch | Extrair para `handlers.ts` ou `dispatch.ts` |
|
||||
|
||||
---
|
||||
|
||||
## 2️⃣ Estrutura de Componentes Vue
|
||||
|
||||
### `defineComponent` obrigatório
|
||||
|
||||
Todo componente Vue deve usar `defineComponent` para garantir inferência de tipos correta e consistência.
|
||||
|
||||
```vue
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue'
|
||||
|
||||
export default defineComponent({
|
||||
name: 'MeuComponente',
|
||||
setup() {
|
||||
// lógica aqui
|
||||
}
|
||||
})
|
||||
</script>
|
||||
```
|
||||
|
||||
**Por que é importante:**
|
||||
- Inferência de tipos TypeScript correta dentro do componente
|
||||
- Reconhecimento por ferramentas de análise estática e IDEs
|
||||
- Compatível com Options API e Composition API com `setup()`
|
||||
- Padronização entre todos os componentes do projeto
|
||||
|
||||
---
|
||||
|
||||
### `setup()` não pode ser externo
|
||||
|
||||
O `setup()` deve estar sempre dentro do componente `.vue`. Não é permitido extrair ou delegar o `setup` para nenhum arquivo externo.
|
||||
|
||||
```ts
|
||||
// ❌ Proibido — setup delegado inteiramente para arquivo externo
|
||||
// useEliTabela.ts
|
||||
export function useEliTabela(props, emit) { ... }
|
||||
|
||||
// EliTabela.vue
|
||||
export default defineComponent({
|
||||
setup(props, { emit }) {
|
||||
return useEliTabela(props, emit) // setup é apenas um proxy vazio
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
```ts
|
||||
// ✅ Correto — setup com lógica própria dentro do componente
|
||||
// EliTabela.vue
|
||||
export default defineComponent({
|
||||
setup(props, { emit }) {
|
||||
const estado = ref(null)
|
||||
const carregar = async () => { ... }
|
||||
return { estado, carregar }
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
> Composables `use*.ts` podem encapsular lógica reutilizável pontual (ex: `useFormatador`, `usePermissao`), mas nunca substituir ou esvaziar o `setup()` do componente. Se o componente ultrapassar 250 linhas, a solução é extrair subcomponentes, mover CSS ou usar Dispatch Table — não esvaziar o `setup`.
|
||||
|
||||
---
|
||||
|
||||
### Desassociação de CSS
|
||||
|
||||
A separação de estilos em arquivo externo é permitida e recomendada quando o CSS contribui significativamente para o total de linhas.
|
||||
|
||||
**Padrão de nomenclatura:**
|
||||
|
||||
```
|
||||
MeuComponente.vue
|
||||
MeuComponente.css
|
||||
```
|
||||
|
||||
O `.vue` importa o arquivo CSS no script:
|
||||
|
||||
```ts
|
||||
import './MeuComponente.css'
|
||||
```
|
||||
|
||||
Ou via tag `<style>`:
|
||||
|
||||
```vue
|
||||
<style src="./MeuComponente.css" />
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Dispatch Table em Arquivo Separado
|
||||
|
||||
O uso de Dispatch Table (mapa de funções) em arquivos separados é bem-vindo para eliminar blocos longos de `if/else` ou `switch`.
|
||||
|
||||
```ts
|
||||
// handlers.ts
|
||||
export const acaoHandlers: Record<string, () => void> = {
|
||||
salvar: () => { ... },
|
||||
cancelar: () => { ... },
|
||||
excluir: () => { ... },
|
||||
}
|
||||
```
|
||||
|
||||
```ts
|
||||
// MeuComponente.vue
|
||||
import { acaoHandlers } from './handlers'
|
||||
|
||||
function executar(acao: string) {
|
||||
acaoHandlers[acao]?.()
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3️⃣ Documentação em Código (JSDoc)
|
||||
|
||||
### Princípios básicos
|
||||
|
||||
- **Por que, não o Quê**: Documente o raciocínio por trás da decisão arquitetural, não o mecanismo óbvio que o TypeScript já expressa.
|
||||
- **Nomenclatura limpa**: Confie na auto-documentação de nomes expressivos antes de escrever comentários de texto livre.
|
||||
|
||||
### Funções compartilhadas — JSDoc obrigatório
|
||||
|
||||
Toda função central ou de uso compartilhado (frontend, backend, mobile ou bibliotecas comuns) deve ter cabeçalho em `JSDoc`.
|
||||
|
||||
```typescript
|
||||
/**
|
||||
* Busca os detalhes de uma entidade baseando-se no ID fornecido e calcula privilégios aplicáveis.
|
||||
* Não utilizar em fluxos de processamento em lote para evitar sobrecarga de requisições.
|
||||
*
|
||||
* @param tenantId - Identificador único do cliente/tenant no sistema
|
||||
* @param entityId - ID exclusivo identificando o registro na base de dados
|
||||
* @returns Retorna o objeto consolidado com dados ou `null` caso não seja encontrado
|
||||
*/
|
||||
export const obterDetalhesEntidade = async (tenantId: string, entityId: string) => { ... }
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4️⃣ Casts Temporários e Exceções de Tipagem
|
||||
|
||||
Devido à política **TypeScript sem any**, coerções agressivas requerem justificativa obrigatória em comentário.
|
||||
|
||||
```typescript
|
||||
// Necessário cast duplo pois a biblioteca externa não provê tipagem genérica adequada na versão atual
|
||||
const dados = payload.raw as unknown as RespostaFormatada
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5️⃣ TODOs Estruturados
|
||||
|
||||
Toda dívida técnica ou dúvida de negócio deixada em código deve seguir o formato `// TODO`:
|
||||
|
||||
- **Tipagem**: `// TODO de tipagem: Criar interface estrita para o retorno da API de listagem`
|
||||
- **Negócio**: `// TODO de regra: Validar com Produto se a taxa adicional se aplica a clientes antigos`
|
||||
|
||||
---
|
||||
|
||||
## 6️⃣ Resumo das Regras
|
||||
|
||||
| Regra | Permitido | Proibido |
|
||||
|---|---|---|
|
||||
| Tamanho de arquivo | Até 250 linhas | Acima de 250 linhas |
|
||||
| `defineComponent` | Obrigatório em todo `.vue` | Componentes sem `defineComponent` |
|
||||
| `setup()` externo | — | Proibido em qualquer forma |
|
||||
| CSS externo | `<nome>.css` vinculado ao `.vue` | CSS inline quando ultrapassa o limite |
|
||||
| Dispatch Table | Em arquivo separado | — |
|
||||
| JSDoc | Obrigatório em funções compartilhadas | Omitir em funções de uso amplo |
|
||||
| Casts `as any` | Com `biome-ignore` justificado | Sem comentário explicativo |
|
||||
|
||||
---
|
||||
|
||||
## 7️⃣ Checklist de Validação
|
||||
|
||||
Antes de concluir qualquer refatoração ou adição de código:
|
||||
|
||||
- [ ] O arquivo resultante tem menos de 250 linhas?
|
||||
- [ ] Funções compartilhadas têm `JSDoc` com `@param` e `@returns`?
|
||||
- [ ] Casts temporários têm comentário justificando?
|
||||
- [ ] TODOs estão no formato padronizado?
|
||||
- [ ] A documentação explica o **porquê**, não apenas o **o quê**?
|
||||
- [ ] `defineComponent` está presente em todo componente `.vue`?
|
||||
|
||||
---
|
||||
|
||||
## Referências
|
||||
|
||||
- Script: `scripts/check-vue-linhas.sh` (validação automática de tamanho)
|
||||
- Rule: `.agent/rules/vue-limite-linhas.global.md` (política de limite de linhas)
|
||||
- Rule: `.agent/rules/typescript-sem-any.global.md` (política de tipagem)
|
||||
- Skill: `.agent/skills/documentar-decisao-tecnica/SKILL.md` (ADRs para decisões arquiteturais)
|
||||
289
.agent/workflows/ux.global.md
Normal file
289
.agent/workflows/ux.global.md
Normal file
|
|
@ -0,0 +1,289 @@
|
|||
---
|
||||
description: Revisar e otimizar UX de componentes e telas Vue (Frontend)
|
||||
---
|
||||
|
||||
# Workflow: Revisão de UX
|
||||
|
||||
Guia de revisão e otimização de interfaces Vue 3 + Vuetify 3 no ecossistema Azteca — para usuários corporativos com baixa familiaridade tecnológica.
|
||||
|
||||
---
|
||||
|
||||
## Perfil do Usuário
|
||||
|
||||
O público-alvo das interfaces é corporativo e diversificado em experiência técnica:
|
||||
|
||||
- Gestores públicos e ambientais
|
||||
- Técnicos de campo
|
||||
- Consultores ambientais
|
||||
- Operadores administrativos
|
||||
|
||||
**Premissa central**: o usuário não tem familiaridade avançada com tecnologia. A interface precisa ser autoexplicativa, sem dependência de treinamento prévio.
|
||||
|
||||
---
|
||||
|
||||
## Paleta de Cores Institucional
|
||||
|
||||
Cor base: `#688C00` (verde floresta). Três escalas derivadas: **Verde floresta** (primária), **Âmbar terroso** (apoio/aviso), **Cinza musgo** (neutros). Semânticas independentes para garantir reconhecimento universal.
|
||||
|
||||
### Escala primária — Verde floresta
|
||||
|
||||
| Token | Hex | Stop | Uso |
|
||||
|---|---|---|---|
|
||||
| `--cor-eli-primaria-bg` | `#EEF3D0` | 50 | Fundo de cards destacados, linhas ativas, hover suave |
|
||||
| `--cor-eli-primaria-zebra` | `#F6F9E8` | 30 | Linha zebrada de tabelas (quase branco esverdeado) |
|
||||
| `--cor-eli-primaria-suave` | `#BFC99C` | — | **Cabeçalho de tabela** — sage/olive mudo, sem limão, texto escuro |
|
||||
| `--cor-eli-primaria-clara` | `#D4E290` | 100 | Badges, chips, seleção, fundo de hover em listas |
|
||||
| `--cor-eli-primaria-media` | `#ADCC44` | 200 | Tags, indicadores secundários |
|
||||
| `--cor-eli-primaria-acao` | `#8AAF00` | 400 | Ação secundária, ícones ativos |
|
||||
| `--cor-eli-primaria` | `#688C00` | 600 | **Botões principais, links ativos, destaques — CTA principal** |
|
||||
| `--cor-eli-primaria-escura` | `#4A6500` | 800 | Hover em botões, bordas ativas, títulos com ênfase |
|
||||
| `--cor-eli-primaria-texto` | `#2E3F00` | 900 | Texto escuro sobre fundos claros da paleta primária |
|
||||
|
||||
### Escala de apoio — Âmbar terroso
|
||||
|
||||
| Token | Hex | Stop | Uso |
|
||||
|---|---|---|---|
|
||||
| `--cor-eli-alerta-bg` | `#FAF3DF` | 50 | Fundo de banners de aviso |
|
||||
| `--cor-eli-alerta-clara` | `#F5DC8A` | 100 | Badges de atenção, chips de alerta |
|
||||
| `--cor-eli-alerta` | `#DDB847` | 200 | Ícones de aviso, bordas de alerta |
|
||||
| `--cor-eli-alerta-forte` | `#8C6A00` | 600 | Texto sobre fundo de alerta, status pendente |
|
||||
|
||||
### Escala neutra — Cinza musgo
|
||||
|
||||
| Token | Hex | Stop | Uso |
|
||||
|---|---|---|---|
|
||||
| `--cor-eli-fundo` | `#F2F3EE` | 50 | Background geral de páginas |
|
||||
| `--cor-eli-borda` | `#D5D8CC` | 100 | Bordas padrão de containers |
|
||||
| `--cor-eli-borda-forte` | `#B5BAA8` | 200 | Bordas de foco, divisores |
|
||||
| `--cor-eli-neutro` | `#5E6553` | 600 | Textos secundários, ícones inativos |
|
||||
| `--cor-eli-neutro-escuro` | `#3E4436` | 800 | Texto de label, cabeçalhos de tabela |
|
||||
| `--cor-eli-superficie` | `#FFFFFF` | — | Cards, modais, popovers |
|
||||
|
||||
### Cores semânticas
|
||||
|
||||
| Token | Hex | Uso |
|
||||
|---|---|---|
|
||||
| `--cor-eli-sucesso` | `#2e7d32` | Confirmações, status positivo |
|
||||
| `--cor-eli-sucesso-bg` | `#E8F5E9` | Fundo de mensagens de sucesso |
|
||||
| `--cor-eli-erro` | `#c62828` | Erros, ações destrutivas |
|
||||
| `--cor-eli-erro-bg` | `#FFEBEE` | Fundo de mensagens de erro |
|
||||
| `--cor-eli-info` | `#1565c0` | Informações auxiliares |
|
||||
| `--cor-eli-info-bg` | `#E3F2FD` | Fundo de banners informativos |
|
||||
|
||||
### Regras de contraste
|
||||
|
||||
- Texto sobre `--cor-eli-primaria` (`#688C00`) deve ser **branco** (`#ffffff`) — contraste 4.6:1 ✓
|
||||
- Texto sobre `--cor-eli-primaria-bg` (`#EEF3D0`) deve ser `--cor-eli-primaria-texto` (`#2E3F00`) — contraste 12:1 ✓
|
||||
- Texto sobre `--cor-eli-primaria-escura` (`#4A6500`) deve ser **branco** (`#ffffff`) — contraste 7.1:1 ✓
|
||||
- Texto sobre `--cor-eli-alerta-bg` (`#FAF3DF`) deve ser `--cor-eli-alerta-forte` (`#8C6A00`) — contraste 5.2:1 ✓
|
||||
- Texto sobre `--cor-eli-primaria-suave` (`#BFC99C`) deve ser `--cor-eli-neutro-escuro` (`#3E4436`) — contraste ~12:1 ✓
|
||||
- **⚠️ Efeito limão**: `--cor-eli-media` (`#ADCC44`) e `--cor-eli-acao` (`#8AAF00`) têm saturação 100% no hue amarelo-verde — **nunca usar como fundo de cabeçalho**. Para fundo verde mudo, usar `--cor-eli-primaria-suave`.
|
||||
- Sempre validar contraste mínimo de **4.5:1** (WCAG AA) para texto normal e **3:1** para texto grande (18px+ ou 14px bold).
|
||||
|
||||
---
|
||||
|
||||
## Passos
|
||||
|
||||
### 1. Identificar o escopo da revisão
|
||||
|
||||
Determinar qual componente, tela ou modal será revisado. Pode ser:
|
||||
|
||||
- Um componente isolado (ex: `EliTabela`, `EliCartao`)
|
||||
- Uma página completa (ex: `PaginaCadastro.vue`)
|
||||
- Um modal ou drawer (ex: `EliTabelaModalFiltroAvancado.vue`)
|
||||
- Uma interação específica (ex: fluxo de filtro, paginação, menu de ações)
|
||||
|
||||
### 2. Mapear o estado atual
|
||||
|
||||
Antes de alterar, entender o que já existe:
|
||||
|
||||
- Ler o template (`<template>`) para entender a estrutura visual
|
||||
- Ler o CSS/SCSS associado para mapear os tokens de design em uso
|
||||
- Identificar quais componentes Vuetify estão sendo utilizados (ex: `v-btn`, `v-dialog`, `v-select`)
|
||||
- Verificar se existem estilos inline que deveriam migrar para classes CSS
|
||||
|
||||
### 3. Avaliar contra os critérios de UX
|
||||
|
||||
Para cada elemento da interface, avaliar:
|
||||
|
||||
#### Clareza
|
||||
|
||||
- [ ] O propósito de cada elemento é óbvio sem instrução?
|
||||
- [ ] Labels e placeholders são descritivos? (ex: "Buscar por nome ou CPF" vs "Buscar")
|
||||
- [ ] Ícones acompanham texto quando a ação não é universalmente reconhecível?
|
||||
- [ ] O estado vazio (empty state) orienta o usuário sobre o próximo passo?
|
||||
|
||||
#### Hierarquia visual
|
||||
|
||||
- [ ] O olho do usuário é guiado para a ação principal primeiro?
|
||||
- [ ] Ações primárias usam `--cor-eli-primaria` (`#688C00`) com destaque?
|
||||
- [ ] Ações secundárias são visuais mas não competem com a primária?
|
||||
- [ ] Ações destrutivas (Excluir, Limpar) usam `--cor-eli-erro` com variante `text` ou `outlined`?
|
||||
|
||||
#### Feedback e estados
|
||||
|
||||
- [ ] Botões têm estados claros de hover (`--cor-eli-primaria-escura`), focus e disabled (`--cor-eli-borda-forte`)?
|
||||
- [ ] Carregamentos exibem indicador visual (spinner, skeleton, barra de progresso)?
|
||||
- [ ] Erros de formulário aparecem próximos ao campo com mensagem descritiva, usando `--cor-eli-erro` + `--cor-eli-erro-bg`?
|
||||
- [ ] Sucesso de operações tem confirmação visual (snackbar, toast, badge) com `--cor-eli-sucesso`?
|
||||
|
||||
#### Densidade e espaçamento
|
||||
|
||||
- [ ] O espaçamento segue múltiplos de 4px (4, 8, 12, 16, 24, 32)?
|
||||
- [ ] Em desktop, o espaço horizontal é bem aproveitado? (ex: grids de 2+ colunas para formulários largos)
|
||||
- [ ] Em mobile, os elementos empilham verticalmente sem quebra visual?
|
||||
- [ ] Modais têm largura proporcional ao conteúdo?
|
||||
|
||||
#### Tipografia
|
||||
|
||||
- [ ] Tamanhos de fonte seguem a escala: 12px (caption), 14px (body), 16px (subtitle), 20px (title), 24px (heading)?
|
||||
- [ ] Textos longos usam `line-height: 1.5` para legibilidade?
|
||||
- [ ] Labels de formulário são visualmente distintos do conteúdo preenchido?
|
||||
|
||||
#### Acessibilidade básica
|
||||
|
||||
- [ ] Elementos interativos têm `aria-label` quando o texto visual não é suficiente?
|
||||
- [ ] Foco via teclado (`Tab`) segue uma ordem lógica?
|
||||
- [ ] Cores não são o único meio de comunicar estado (ex: ícone + cor para erro)?
|
||||
- [ ] Todo texto sobre cor primária ou escura usa branco (`#FFFFFF`)?
|
||||
- [ ] Todo texto sobre `--cor-eli-primaria-bg` usa `--cor-eli-primaria-texto` (`#2E3F00`)?
|
||||
|
||||
### 4. Aplicar padrões Vue/Vuetify do projeto
|
||||
|
||||
Ao implementar melhorias, seguir os padrões do ecossistema:
|
||||
|
||||
#### Componentes Vuetify
|
||||
|
||||
- Usar `v-btn` com variantes do tema (`color="primary"`) em vez de classes CSS manuais para botões
|
||||
- Usar `v-dialog` para modais, `v-menu` para popovers contextuais
|
||||
- Usar `v-text-field`, `v-select` etc. para inputs sempre que possível
|
||||
- Aproveitar props de Vuetify: `density`, `variant`, `rounded`, `elevation`
|
||||
|
||||
#### Iconografia — Lucide (obrigatório)
|
||||
|
||||
Todos os ícones do projeto devem vir exclusivamente da biblioteca **`@lucide/vue`** (dependência já instalada no `package.json`).
|
||||
|
||||
**Proibido**:
|
||||
- SVG inline manual (hardcoded no template)
|
||||
- Ícones de `@mdi/font` ou Material Design Icons em componentes do projeto
|
||||
- Bibliotecas alternativas (Font Awesome, Heroicons, etc.)
|
||||
|
||||
**Como usar**:
|
||||
|
||||
```vue
|
||||
<script lang="ts">
|
||||
import { Filter, Plus, X, Trash2, Eye } from "@lucide/vue"
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<!-- Uso direto como componente -->
|
||||
<Filter :size="18" :stroke-width="2" />
|
||||
|
||||
<!-- Dentro de botão -->
|
||||
<button>
|
||||
<Plus :size="15" :stroke-width="2.5" />
|
||||
Adicionar
|
||||
</button>
|
||||
</template>
|
||||
```
|
||||
|
||||
**Props úteis**: `:size` (número em px), `:stroke-width` (espessura do traço, padrão 2), `:color` (cor do ícone).
|
||||
|
||||
**Catálogo de referência**: https://lucide.dev/icons — buscar pelo nome do ícone em inglês.
|
||||
|
||||
#### CSS do projeto
|
||||
|
||||
```css
|
||||
/* Escala primária — Verde floresta */
|
||||
--cor-eli-primaria-bg: #EEF3D0;
|
||||
--cor-eli-primaria-zebra: #F6F9E8;
|
||||
--cor-eli-primaria-suave: #BFC99C;
|
||||
--cor-eli-primaria-clara: #D4E290;
|
||||
--cor-eli-primaria-media: #ADCC44;
|
||||
--cor-eli-primaria-acao: #8AAF00;
|
||||
--cor-eli-primaria: #688C00;
|
||||
--cor-eli-primaria-escura: #4A6500;
|
||||
--cor-eli-primaria-texto: #2E3F00;
|
||||
|
||||
/* Escala de apoio — Âmbar terroso */
|
||||
--cor-eli-alerta-bg: #FAF3DF;
|
||||
--cor-eli-alerta-clara: #F5DC8A;
|
||||
--cor-eli-alerta: #DDB847;
|
||||
--cor-eli-alerta-forte: #8C6A00;
|
||||
|
||||
/* Escala neutra — Cinza musgo */
|
||||
--cor-eli-fundo: #F2F3EE;
|
||||
--cor-eli-borda: #D5D8CC;
|
||||
--cor-eli-borda-forte: #B5BAA8;
|
||||
--cor-eli-neutro: #5E6553;
|
||||
--cor-eli-neutro-escuro: #3E4436;
|
||||
--cor-eli-superficie: #FFFFFF;
|
||||
|
||||
/* Semânticas */
|
||||
--cor-eli-sucesso: #2E7D32;
|
||||
--cor-eli-sucesso-bg: #E8F5E9;
|
||||
--cor-eli-erro: #C62828;
|
||||
--cor-eli-erro-bg: #FFEBEE;
|
||||
--cor-eli-info: #1565C0;
|
||||
--cor-eli-info-bg: #E3F2FD;
|
||||
```
|
||||
|
||||
> **⚠️ Regra crítica para componentes distribuídos como biblioteca**: todo token CSS novo (`--cor-eli-primaria-zebra`, `--cor-eli-primaria-suave`, etc.) deve ter **fallback hardcoded** no CSS do componente: `var(--token, #valorhex)`. Projetos destino podem não ter a versão mais recente do `design-tokens.css` e a variável resolverá como `undefined`, tornando a propriedade invisível.
|
||||
|
||||
- Classes devem seguir BEM: `eli-componente__elemento--modificador`
|
||||
- Evitar `!important` — resolver especificidade via seletor mais específico
|
||||
- Usar `var(--cor-eli-primaria)` em vez de valores hex hardcoded no CSS dos componentes
|
||||
- Transições padrão: `transition: all 0.2s ease` para hover/focus
|
||||
|
||||
#### Composição de componentes
|
||||
|
||||
- Componentes filhos devem receber dados via `props` e emitir eventos via `$emit`
|
||||
- Evitar acessar o DOM diretamente; preferir `ref` do Vue
|
||||
- Não duplicar CSS entre componentes — extrair para classe utilitária ou CSS do componente pai
|
||||
|
||||
### 5. Implementar melhorias prioritárias
|
||||
|
||||
Ordenar as melhorias por impacto no usuário:
|
||||
|
||||
1. **Crítico**: elementos que causam confusão ou impedem o uso (falta de feedback, botão invisível, estado vazio sem orientação)
|
||||
2. **Alto**: melhorias que reduzem atrito (espaçamento, hierarquia de botões, responsividade)
|
||||
3. **Médio**: refinamentos visuais (transições, micro-animações, consistência de cores)
|
||||
4. **Baixo**: polish (sombras sutis, arredondamentos, tipografia refinada)
|
||||
|
||||
### 6. Validar resultado
|
||||
|
||||
Após implementar:
|
||||
|
||||
- [ ] A tela renderiza sem erros no console?
|
||||
- [ ] Os estados (vazio, carregando, erro, com dados) estão todos cobertos?
|
||||
- [ ] A paleta de cores está coerente com a base `#688C00`?
|
||||
- [ ] Todos os tokens usados estão definidos na escala primária, de apoio ou neutra?
|
||||
- [ ] Nenhuma cor está hardcoded no CSS do componente (usar `var(--token, #fallback)`)?
|
||||
- [ ] Tokens novos têm fallback hardcoded? (ex: `var(--cor-eli-primaria-suave, #BFC99C)`)?
|
||||
- [ ] Contraste de texto validado: branco sobre `--cor-eli-primaria` e `--cor-eli-primaria-escura`; `--cor-eli-neutro-escuro` sobre `--cor-eli-primaria-suave`?
|
||||
- [ ] Nenhuma ação ficou sem feedback visual?
|
||||
- [ ] O layout não quebra em larguras menores que 1024px?
|
||||
- [ ] Componentes Vuetify recebem props de tema (`color`, `variant`) em vez de override CSS?
|
||||
- [ ] Efeito zebra de tabelas tem fundo branco explícito nas linhas normais (para funcionar sobre qualquer fundo de página)?
|
||||
|
||||
---
|
||||
|
||||
## Anti-padrões a evitar
|
||||
|
||||
| ❌ Anti-padrão | ✅ Alternativa |
|
||||
|---|---|
|
||||
| Botão sem label de texto | Botão com ícone + texto, ou `aria-label` descritivo |
|
||||
| Cor vermelha para ação primária | Vermelho apenas para destrutivas; primária usa `--cor-eli-primaria` |
|
||||
| Modal com scroll interno extenso | Dividir em etapas ou usar drawer lateral |
|
||||
| Tabela com 15+ colunas visíveis | Usar o modal de colunas para esconder as menos usadas |
|
||||
| Placeholder como único label | Label fixo acima do campo + placeholder como exemplo |
|
||||
| Ações alinhadas à esquerda em linha de tabela | Ações à direita, próximas ao fim da linha visual |
|
||||
| Toast/snackbar sem ação de fechar | Sempre incluir botão de fechar ou auto-dismiss com tempo suficiente (5s+) |
|
||||
| Estilos inline em `<template>` | Migrar para `<style scoped>` com classe BEM |
|
||||
| Cor hex hardcoded no componente | Usar `var(--cor-eli-primaria)` e demais tokens com fallback |
|
||||
| Texto escuro (`#333`) sobre `--cor-eli-primaria` | Usar branco (`#FFFFFF`) para garantir contraste WCAG AA |
|
||||
| Amarelo/âmbar para sucesso | Âmbar é reservado para **aviso**; sucesso usa `--cor-eli-sucesso` (`#2E7D32`) |
|
||||
| Misturar stops da paleta fora dos tokens | Usar apenas os tokens definidos; nunca interpolar valores avulsos |
|
||||
| Usar `--cor-eli-media` ou `--cor-eli-acao` como fundo de cabeçalho | Usar `--cor-eli-primaria-suave` (`#BFC99C`) — verde mudo sem efeito limão |
|
||||
| `var(--token)` sem fallback em componente de biblioteca | Sempre declarar `var(--token, #valor)` — o projeto destino pode não ter o token atualizado |
|
||||
| Zebra de tabela sem fundo branco explícito nas linhas normais | Declarar `.eli-tabela__tr .eli-tabela__td { background: var(--cor-eli-superficie, #fff) }` antes da regra zebra |
|
||||
13476
dist/eli-vue.es.js
vendored
13476
dist/eli-vue.es.js
vendored
File diff suppressed because one or more lines are too long
298
dist/eli-vue.umd.js
vendored
298
dist/eli-vue.umd.js
vendored
File diff suppressed because one or more lines are too long
51
dist/types/componentes/EliEntrada/EliEntradaNumero.helpers.d.ts
vendored
Normal file
51
dist/types/componentes/EliEntrada/EliEntradaNumero.helpers.d.ts
vendored
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
/**
|
||||
* Helpers de formatação e parsing numérico usados pelo EliEntradaNumero.
|
||||
* Extraídos do componente Vue para manter o arquivo dentro de 250 linhas.
|
||||
*/
|
||||
export declare function casasDecimaisFromPrecisao(precisao: number): number;
|
||||
/**
|
||||
* Analisa a entrada de texto e converte em um valor numérico válido (float).
|
||||
* Troca a vírgula brasileira por ponto antes de passar para o parser nativo.
|
||||
*
|
||||
* @param texto - O texto bruto digitado ou vindo do estado
|
||||
* @returns O número processado ou `null` caso inválido ou vazio
|
||||
*/
|
||||
export declare function parseNumero(texto: string): number | null;
|
||||
/**
|
||||
* Formata um valor numérico para a exibição na interface (usando vírgula como separador).
|
||||
*
|
||||
* @param value - O valor numérico de fato emitido
|
||||
* @param casas - Opcionalmente, o número exato de casas decimais para fixar
|
||||
* @returns String formatada amigável para exibição em pt-BR
|
||||
*/
|
||||
export declare function formatarNumero(value: number | null | undefined, casas: number | null): string;
|
||||
/**
|
||||
* Retira todo tipo de sujeira de uma string, devolvendo restritamente números (para inteiros).
|
||||
*
|
||||
* @param texto - String de entrada
|
||||
* @returns Apenas dígitos de 0 a 9
|
||||
*/
|
||||
export declare function somenteNumeros(texto: string): string;
|
||||
/**
|
||||
* Higieniza o texto preservando apenas os dígitos e exatamente UM separador decimal.
|
||||
*
|
||||
* @param texto - Texto misto de entrada
|
||||
* @returns String contendo número e (opcionalmente) uma vírgula ou ponto residual
|
||||
*/
|
||||
export declare function somenteNumerosESeparadorDecimal(texto: string): string;
|
||||
/**
|
||||
* Trunca a parte decimal de um texto numérico ao número máximo de casas permitidas.
|
||||
* Aplicado durante a digitação para evitar que o usuário ultrapasse a precisão configurada.
|
||||
*
|
||||
* @param texto - Texto numérico já higienizado (com separador único)
|
||||
* @param casas - Número máximo de casas decimais; `null` = sem limite
|
||||
* @returns Texto com a parte decimal truncada no limite especificado
|
||||
*/
|
||||
export declare function limitarCasasDecimais(texto: string, casas: number | null): string;
|
||||
/**
|
||||
* Resolve o edge-case de digitação transitória parcial terminada em separador (ex: "1,").
|
||||
*
|
||||
* @param texto - O texto já higienizado, tipicamente terminando em vírgula ou ponto
|
||||
* @returns A conversão parcial estrita ou `null` caso falhe
|
||||
*/
|
||||
export declare function valorParcialDecimal(texto: string): number | null;
|
||||
|
|
@ -1,5 +1,6 @@
|
|||
import { type PropType } from "vue";
|
||||
import type { PadroesEntradas } from "./tiposEntradas";
|
||||
import "./EliEntradaNumero.css";
|
||||
type EntradaNumero = PadroesEntradas["numero"];
|
||||
declare const _default: typeof __VLS_export;
|
||||
export default _default;
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
|
@ -1 +1,9 @@
|
|||
/**
|
||||
* Formata um CPF ou CNPJ aplicando a máscara brasileira padrão durante a digitação.
|
||||
* Detecta automaticamente o tipo pelo comprimento (≤11 dígitos = CPF, >11 = CNPJ).
|
||||
* Não valida o dígito verificador — apenas aplica a formatação visual.
|
||||
*
|
||||
* @param v - Valor bruto (pode conter letras, pontos, traços etc.)
|
||||
* @returns String formatada no padrão `000.000.000-00` (CPF) ou `00.000.000/0000-00` (CNPJ)
|
||||
*/
|
||||
export declare function formatarCpfCnpj(v: string): string;
|
||||
|
|
|
|||
533
dist/types/componentes/EliTabela/EliTabela.vue.d.ts
vendored
533
dist/types/componentes/EliTabela/EliTabela.vue.d.ts
vendored
|
|
@ -1,8 +1,17 @@
|
|||
/** Dependências do Vue (Composition API) */
|
||||
/**
|
||||
* EliTabela
|
||||
* Componente de tabela consultável com busca, paginação, ordenação e ações por linha.
|
||||
* O setup() é orquestrado por `useEliTabela`, que compõe os seguintes sub-composables:
|
||||
* - useEliTabelaConsulta — carregamento, paginação, ordenação, busca
|
||||
* - useEliTabelaFiltrosAvancados — filtros avançados, modal e persistência
|
||||
* - useEliTabelaSelecao — seleção múltipla de linhas
|
||||
* - useEliTabelaColunas — visibilidade, ordem e modal de colunas
|
||||
* - useEliTabelaMenu — menu de ações por linha e posicionamento de popup
|
||||
*/
|
||||
import { type PropType } from "vue";
|
||||
import { type EliTabelaColunasConfig } from "./colunasStorage";
|
||||
/** Tipos da configuração/contrato da tabela */
|
||||
import type { parametrosConsulta, tipoEliColuna, tipoEliTabelaConsulta, tipoFiltro } from "./types-eli-tabela";
|
||||
import "./EliTabela.css";
|
||||
import type { tipoEliTabelaConsulta } from "./types-eli-tabela";
|
||||
import type { FiltroBase } from "./useFiltroAvancado";
|
||||
declare const _default: typeof __VLS_export;
|
||||
export default _default;
|
||||
declare const __VLS_export: import("vue").DefineComponent<import("vue").ExtractPropTypes<{
|
||||
|
|
@ -16,119 +25,19 @@ declare const __VLS_export: import("vue").DefineComponent<import("vue").ExtractP
|
|||
type: PropType<unknown[]>;
|
||||
default: () => never[];
|
||||
};
|
||||
/** Ocultar painel de debug lateral (EliTabelaDebug) */
|
||||
esconderDebug: {
|
||||
type: BooleanConstructor;
|
||||
default: boolean;
|
||||
};
|
||||
}>, {
|
||||
isDev: boolean;
|
||||
tabela: import("vue").ComputedRef<tipoEliTabelaConsulta<any>>;
|
||||
carregando: import("vue").Ref<boolean, boolean>;
|
||||
erro: import("vue").Ref<string | null, string | null>;
|
||||
linhas: import("vue").Ref<unknown[], unknown[]>;
|
||||
linhasPaginadas: import("vue").ComputedRef<unknown[]>;
|
||||
filtrosAvancadosAtivos: import("vue").ComputedRef<tipoFiltro<any>[]>;
|
||||
quantidadeFiltrada: import("vue").ComputedRef<number>;
|
||||
quantidade: import("vue").Ref<number, number>;
|
||||
menuAberto: import("vue").Ref<number | null, number | null>;
|
||||
valorBusca: import("vue").Ref<string, string>;
|
||||
paginaAtual: import("vue").Ref<number, number>;
|
||||
colunaOrdenacao: import("vue").Ref<string | null, string | null>;
|
||||
direcaoOrdenacao: import("vue").Ref<"desc" | "asc", "desc" | "asc">;
|
||||
totalPaginas: import("vue").ComputedRef<number>;
|
||||
registrosPorConsulta: import("vue").ComputedRef<number>;
|
||||
exibirBusca: import("vue").ComputedRef<boolean>;
|
||||
exibirFiltroAvancado: import("vue").ComputedRef<boolean>;
|
||||
acoesSuperiores: import("vue").ComputedRef<{
|
||||
posicao: "superior" | "inferior";
|
||||
icone?: import("@lucide/vue").LucideIcon;
|
||||
cor?: string;
|
||||
rotulo: string;
|
||||
acao: (parametrosConsulta: parametrosConsulta<any> & {
|
||||
atualizarConsulta: () => Promise<void>;
|
||||
editarLista: (lista: any[]) => Promise<any[]>;
|
||||
}) => void;
|
||||
}[]>;
|
||||
temAcoesSuperiores: import("vue").ComputedRef<boolean>;
|
||||
acoesInferiores: import("vue").ComputedRef<{
|
||||
posicao: "superior" | "inferior";
|
||||
icone?: import("@lucide/vue").LucideIcon;
|
||||
cor?: string;
|
||||
rotulo: string;
|
||||
acao: (parametrosConsulta: parametrosConsulta<any> & {
|
||||
atualizarConsulta: () => Promise<void>;
|
||||
editarLista: (lista: any[]) => Promise<any[]>;
|
||||
}) => void;
|
||||
}[]>;
|
||||
temAcoesInferiores: import("vue").ComputedRef<boolean>;
|
||||
parametrosConsultaAtuais: import("vue").ComputedRef<{
|
||||
atualizarConsulta: () => Promise<void>;
|
||||
editarLista: (novaLista: any[]) => Promise<any[]>;
|
||||
filtros?: import("p-comuns").tipoFiltro26<any>[] | undefined;
|
||||
coluna_ordem?: string | number | symbol | undefined;
|
||||
direcao_ordem?: "asc" | "desc";
|
||||
offSet?: number;
|
||||
limit?: number;
|
||||
texto_busca?: string;
|
||||
}>;
|
||||
temAcoes: import("vue").ComputedRef<boolean>;
|
||||
totalColunas: import("vue").ComputedRef<number>;
|
||||
colunasEfetivas: import("vue").ComputedRef<any[]>;
|
||||
rotulosColunas: import("vue").ComputedRef<string[]>;
|
||||
modalColunasAberto: import("vue").Ref<boolean, boolean>;
|
||||
configColunas: import("vue").Ref<{
|
||||
visiveis: string[];
|
||||
invisiveis: string[];
|
||||
}, EliTabelaColunasConfig | {
|
||||
visiveis: string[];
|
||||
invisiveis: string[];
|
||||
}>;
|
||||
temColunasInvisiveis: import("vue").ComputedRef<boolean>;
|
||||
colunasInvisiveisEfetivas: import("vue").ComputedRef<tipoEliColuna<any>[]>;
|
||||
linhasExpandidas: import("vue").Ref<Record<number, boolean>, Record<number, boolean>>;
|
||||
mostrarSelecao: import("vue").ComputedRef<boolean>;
|
||||
isSelecionado: (linha: unknown) => boolean;
|
||||
alternarSelecao: (linha: unknown) => void;
|
||||
todosSelecionados: import("vue").ComputedRef<boolean>;
|
||||
algunsSelecionados: import("vue").ComputedRef<boolean>;
|
||||
alternarSelecaoTodos: (forcarMarcar?: boolean) => void;
|
||||
abrirModalColunas: () => void;
|
||||
abrirModalFiltro: () => void;
|
||||
fecharModalColunas: () => void;
|
||||
salvarModalColunas: (cfg: EliTabelaColunasConfig) => void;
|
||||
modalFiltroAberto: import("vue").Ref<boolean, boolean>;
|
||||
filtrosUi: import("vue").Ref<{
|
||||
chave: string;
|
||||
valor: any;
|
||||
}[], {
|
||||
chave: string;
|
||||
valor: any;
|
||||
}[] | {
|
||||
chave: string;
|
||||
valor: any;
|
||||
}[]>;
|
||||
salvarFiltrosAvancados: (novo: any[]) => void;
|
||||
limparFiltrosAvancados: () => void;
|
||||
fecharModalFiltro: () => void;
|
||||
alternarLinhaExpandida: (indice: number) => void;
|
||||
alternarOrdenacao: (chave?: string) => void;
|
||||
atualizarBusca: (texto: string) => void;
|
||||
irParaPagina: (pagina: number) => void;
|
||||
acoesDisponiveisPorLinha: (i: number) => {
|
||||
acao: import("./types-eli-tabela").tipoEliTabelaAcao<any>;
|
||||
indice: number;
|
||||
visivel: boolean;
|
||||
}[];
|
||||
possuiAcoes: (i: number) => boolean;
|
||||
toggleMenu: (i: number, evento?: MouseEvent) => void;
|
||||
menuPopup: import("vue").Ref<{
|
||||
menuEl: {
|
||||
value: HTMLElement | null;
|
||||
};
|
||||
menuEl: HTMLElement | null;
|
||||
} | null, {
|
||||
menuEl: {
|
||||
value: HTMLElement | null;
|
||||
};
|
||||
menuEl: HTMLElement | null;
|
||||
} | {
|
||||
menuEl: {
|
||||
value: HTMLElement | null;
|
||||
};
|
||||
menuEl: HTMLElement | null;
|
||||
} | null>;
|
||||
menuPopupPos: import("vue").Ref<{
|
||||
top: number;
|
||||
|
|
@ -140,6 +49,111 @@ declare const __VLS_export: import("vue").DefineComponent<import("vue").ExtractP
|
|||
top: number;
|
||||
left: number;
|
||||
}>;
|
||||
possuiAcoes: (i: number) => boolean;
|
||||
toggleMenu: (i: number, evento?: MouseEvent) => void;
|
||||
acoesDisponiveisPorLinha: (i: number) => {
|
||||
acao: import("./types-eli-tabela").tipoEliTabelaAcao<any>;
|
||||
indice: number;
|
||||
visivel: boolean;
|
||||
}[];
|
||||
paginaAtual: import("vue").Ref<number, number>;
|
||||
valorBusca: import("vue").Ref<string, string>;
|
||||
modalColunasAberto: import("vue").Ref<boolean, boolean>;
|
||||
configColunas: import("vue").Ref<{
|
||||
visiveis: string[];
|
||||
invisiveis: string[];
|
||||
}, import("./colunasStorage").EliTabelaColunasConfig | {
|
||||
visiveis: string[];
|
||||
invisiveis: string[];
|
||||
}>;
|
||||
linhasExpandidas: import("vue").Ref<Record<number, boolean>, Record<number, boolean>>;
|
||||
rotulosColunas: import("vue").ComputedRef<string[]>;
|
||||
colunasEfetivas: import("vue").ComputedRef<any[]>;
|
||||
colunasInvisiveisEfetivas: import("vue").ComputedRef<import("./types-eli-tabela").tipoEliColuna<any>[]>;
|
||||
temColunasInvisiveis: import("vue").ComputedRef<boolean>;
|
||||
totalColunas: import("vue").ComputedRef<number>;
|
||||
abrirModalColunas: () => void;
|
||||
fecharModalColunas: () => void;
|
||||
salvarModalColunas: (cfg: import("./colunasStorage").EliTabelaColunasConfig) => void;
|
||||
alternarLinhaExpandida: (indice: number) => void;
|
||||
mostrarSelecao: import("vue").ComputedRef<boolean>;
|
||||
isSelecionado: (linha: unknown) => boolean;
|
||||
alternarSelecao: (linha: unknown) => void;
|
||||
todosSelecionados: import("vue").ComputedRef<boolean>;
|
||||
algunsSelecionados: import("vue").ComputedRef<boolean>;
|
||||
alternarSelecaoTodos: (forcarMarcar?: boolean) => void;
|
||||
filtrosUi: import("vue").Ref<{
|
||||
chave: string;
|
||||
valor: unknown;
|
||||
}[], {
|
||||
chave: string;
|
||||
valor: unknown;
|
||||
}[] | {
|
||||
chave: string;
|
||||
valor: unknown;
|
||||
}[]>;
|
||||
filtrosFormula: import("vue").Ref<string, string>;
|
||||
modalFiltroAberto: import("vue").Ref<boolean, boolean>;
|
||||
exibirFiltroAvancado: import("vue").ComputedRef<boolean>;
|
||||
filtrosAvancadosAtivos: import("vue").ComputedRef<import("p-comuns").tipoFiltroDeltaColunaTipada<any>[]>;
|
||||
abrirModalFiltro: () => void;
|
||||
fecharModalFiltro: () => void;
|
||||
limparFiltrosAvancados: () => void;
|
||||
salvarFiltrosAvancados: (novoLinhas: {
|
||||
chave: string;
|
||||
valor: unknown;
|
||||
}[], novaFormula?: string) => void;
|
||||
isDev: boolean;
|
||||
tabela: import("vue").ComputedRef<tipoEliTabelaConsulta<any>>;
|
||||
carregando: import("vue").Ref<boolean, boolean>;
|
||||
erro: import("vue").Ref<string | null, string | null>;
|
||||
linhas: import("vue").Ref<unknown[], unknown[]>;
|
||||
linhasPaginadas: import("vue").ComputedRef<unknown[]>;
|
||||
quantidade: import("vue").Ref<number, number>;
|
||||
quantidadeFiltrada: import("vue").ComputedRef<number>;
|
||||
acoesVisiveis: import("vue").Ref<boolean[][], boolean[][]>;
|
||||
colunaOrdenacao: import("vue").Ref<string | null, string | null>;
|
||||
direcaoOrdenacao: import("vue").Ref<"desc" | "asc", "desc" | "asc">;
|
||||
registrosPorConsulta: import("vue").ComputedRef<number>;
|
||||
totalPaginas: import("vue").ComputedRef<number>;
|
||||
temAcoes: import("vue").ComputedRef<boolean>;
|
||||
exibirBusca: import("vue").ComputedRef<boolean>;
|
||||
acoesSuperiores: import("vue").ComputedRef<{
|
||||
posicao: "superior" | "inferior";
|
||||
icone?: import("@lucide/vue").LucideIcon;
|
||||
cor?: string;
|
||||
rotulo: string;
|
||||
acao: (parametrosConsulta: import("./types-eli-tabela").parametrosConsulta<any> & {
|
||||
atualizarConsulta: () => Promise<void>;
|
||||
editarLista: (lista: any[]) => Promise<any[]>;
|
||||
}) => void;
|
||||
}[]>;
|
||||
acoesInferiores: import("vue").ComputedRef<{
|
||||
posicao: "superior" | "inferior";
|
||||
icone?: import("@lucide/vue").LucideIcon;
|
||||
cor?: string;
|
||||
rotulo: string;
|
||||
acao: (parametrosConsulta: import("./types-eli-tabela").parametrosConsulta<any> & {
|
||||
atualizarConsulta: () => Promise<void>;
|
||||
editarLista: (lista: any[]) => Promise<any[]>;
|
||||
}) => void;
|
||||
}[]>;
|
||||
temAcoesSuperiores: import("vue").ComputedRef<boolean>;
|
||||
temAcoesInferiores: import("vue").ComputedRef<boolean>;
|
||||
parametrosConsultaAtuais: import("vue").ComputedRef<{
|
||||
atualizarConsulta: () => Promise<void>;
|
||||
editarLista: (novaLista: any[]) => Promise<any[]>;
|
||||
filtros?: import("p-comuns").tipoFiltroDeltaColunaTipada<string>[] | undefined;
|
||||
coluna_ordem?: string | number | symbol | undefined;
|
||||
direcao_ordem?: "asc" | "desc";
|
||||
offSet?: number;
|
||||
limit?: number;
|
||||
texto_busca?: string;
|
||||
}>;
|
||||
carregar: () => Promise<void>;
|
||||
alternarOrdenacao: (chave?: string) => void;
|
||||
atualizarBusca: (texto: string) => void;
|
||||
irParaPagina: (pagina: number) => void;
|
||||
}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, "update:selecionados"[], "update:selecionados", import("vue").PublicProps, Readonly<import("vue").ExtractPropTypes<{
|
||||
/** Configuração principal da tabela (colunas, consulta e ações) */
|
||||
tabela: {
|
||||
|
|
@ -151,10 +165,16 @@ declare const __VLS_export: import("vue").DefineComponent<import("vue").ExtractP
|
|||
type: PropType<unknown[]>;
|
||||
default: () => never[];
|
||||
};
|
||||
/** Ocultar painel de debug lateral (EliTabelaDebug) */
|
||||
esconderDebug: {
|
||||
type: BooleanConstructor;
|
||||
default: boolean;
|
||||
};
|
||||
}>> & Readonly<{
|
||||
"onUpdate:selecionados"?: ((...args: any[]) => any) | undefined;
|
||||
}>, {
|
||||
selecionados: unknown[];
|
||||
esconderDebug: boolean;
|
||||
}, {}, {
|
||||
EliTabelaCabecalho: import("vue").DefineComponent<import("vue").ExtractPropTypes<{
|
||||
exibirBusca: {
|
||||
|
|
@ -181,28 +201,10 @@ declare const __VLS_export: import("vue").DefineComponent<import("vue").ExtractP
|
|||
};
|
||||
acoesCabecalho: {
|
||||
type: PropType<Array<{
|
||||
icone
|
||||
/**
|
||||
* EliTabela
|
||||
* Componente de tabela consultável com busca, paginação, ordenação e ações por linha.
|
||||
*/
|
||||
/** Enum de códigos de resposta utilizado na consulta */
|
||||
?: any;
|
||||
cor
|
||||
/**
|
||||
* EliTabela
|
||||
* Componente de tabela consultável com busca, paginação, ordenação e ações por linha.
|
||||
*/
|
||||
/** Enum de códigos de resposta utilizado na consulta */
|
||||
?: string;
|
||||
icone?: any;
|
||||
cor?: string;
|
||||
rotulo: string;
|
||||
acao: (params
|
||||
/**
|
||||
* EliTabela
|
||||
* Componente de tabela consultável com busca, paginação, ordenação e ações por linha.
|
||||
*/
|
||||
/** Enum de códigos de resposta utilizado na consulta */
|
||||
?: any) => void;
|
||||
acao: (params?: any) => void;
|
||||
}>>;
|
||||
required: true;
|
||||
};
|
||||
|
|
@ -240,28 +242,10 @@ declare const __VLS_export: import("vue").DefineComponent<import("vue").ExtractP
|
|||
};
|
||||
acoesCabecalho: {
|
||||
type: PropType<Array<{
|
||||
icone
|
||||
/**
|
||||
* EliTabela
|
||||
* Componente de tabela consultável com busca, paginação, ordenação e ações por linha.
|
||||
*/
|
||||
/** Enum de códigos de resposta utilizado na consulta */
|
||||
?: any;
|
||||
cor
|
||||
/**
|
||||
* EliTabela
|
||||
* Componente de tabela consultável com busca, paginação, ordenação e ações por linha.
|
||||
*/
|
||||
/** Enum de códigos de resposta utilizado na consulta */
|
||||
?: string;
|
||||
icone?: any;
|
||||
cor?: string;
|
||||
rotulo: string;
|
||||
acao: (params
|
||||
/**
|
||||
* EliTabela
|
||||
* Componente de tabela consultável com busca, paginação, ordenação e ações por linha.
|
||||
*/
|
||||
/** Enum de códigos de resposta utilizado na consulta */
|
||||
?: any) => void;
|
||||
acao: (params?: any) => void;
|
||||
}>>;
|
||||
required: true;
|
||||
};
|
||||
|
|
@ -273,6 +257,7 @@ declare const __VLS_export: import("vue").DefineComponent<import("vue").ExtractP
|
|||
exibirBotaoColunas: boolean;
|
||||
exibirBotaoFiltroAvancado: boolean;
|
||||
}, {}, {
|
||||
Columns3: import("@lucide/vue").LucideIcon;
|
||||
EliTabelaCaixaDeBusca: import("vue").DefineComponent<import("vue").ExtractPropTypes<{
|
||||
modelo: {
|
||||
type: StringConstructor;
|
||||
|
|
@ -297,6 +282,7 @@ declare const __VLS_export: import("vue").DefineComponent<import("vue").ExtractP
|
|||
}, {}, {
|
||||
Search: import("@lucide/vue").LucideIcon;
|
||||
}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
|
||||
Filter: import("@lucide/vue").LucideIcon;
|
||||
}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
|
||||
EliTabelaEstados: import("vue").DefineComponent<import("vue").ExtractPropTypes<{
|
||||
carregando: {
|
||||
|
|
@ -329,7 +315,11 @@ declare const __VLS_export: import("vue").DefineComponent<import("vue").ExtractP
|
|||
}>> & Readonly<{}>, {
|
||||
erro: string | null;
|
||||
mensagemVazio: string | undefined;
|
||||
}, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
|
||||
}, {}, {
|
||||
AlertCircle: import("@lucide/vue").LucideIcon;
|
||||
Inbox: import("@lucide/vue").LucideIcon;
|
||||
LoaderCircle: import("@lucide/vue").LucideIcon;
|
||||
}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
|
||||
EliTabelaDebug: import("vue").DefineComponent<import("vue").ExtractPropTypes<{
|
||||
isDev: {
|
||||
type: BooleanConstructor;
|
||||
|
|
@ -346,7 +336,7 @@ declare const __VLS_export: import("vue").DefineComponent<import("vue").ExtractP
|
|||
}>;
|
||||
required: true;
|
||||
};
|
||||
}>, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<import("vue").ExtractPropTypes<{
|
||||
}>, void, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<import("vue").ExtractPropTypes<{
|
||||
isDev: {
|
||||
type: BooleanConstructor;
|
||||
required: true;
|
||||
|
|
@ -367,7 +357,7 @@ declare const __VLS_export: import("vue").DefineComponent<import("vue").ExtractP
|
|||
}, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
|
||||
EliTabelaHead: import("vue").DefineComponent<import("vue").ExtractPropTypes<{
|
||||
colunas: {
|
||||
type: PropType<Array<tipoEliColuna<any>>>;
|
||||
type: PropType<import("./types-eli-tabela").tipoEliColuna<any>[]>;
|
||||
required: true;
|
||||
};
|
||||
temAcoes: {
|
||||
|
|
@ -401,14 +391,14 @@ declare const __VLS_export: import("vue").DefineComponent<import("vue").ExtractP
|
|||
}>, {
|
||||
ArrowUp: import("@lucide/vue").LucideIcon;
|
||||
ArrowDown: import("@lucide/vue").LucideIcon;
|
||||
isOrdenavel: (coluna: any) => boolean;
|
||||
isOrdenavel: (coluna: import("./types-eli-tabela").tipoEliColuna<unknown>) => boolean;
|
||||
emitAlternarOrdenacao: (chave: string) => void;
|
||||
}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
|
||||
alternarOrdenacao(chave: string): boolean;
|
||||
"alternar-selecao-todos": () => true;
|
||||
}, string, import("vue").PublicProps, Readonly<import("vue").ExtractPropTypes<{
|
||||
colunas: {
|
||||
type: PropType<Array<tipoEliColuna<any>>>;
|
||||
type: PropType<import("./types-eli-tabela").tipoEliColuna<any>[]>;
|
||||
required: true;
|
||||
};
|
||||
temAcoes: {
|
||||
|
|
@ -453,11 +443,11 @@ declare const __VLS_export: import("vue").DefineComponent<import("vue").ExtractP
|
|||
}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
|
||||
EliTabelaBody: import("vue").DefineComponent<import("vue").ExtractPropTypes<{
|
||||
colunas: {
|
||||
type: PropType<Array<tipoEliColuna<any>>>;
|
||||
type: PropType<import("./types-eli-tabela").tipoEliColuna<any>[]>;
|
||||
required: true;
|
||||
};
|
||||
colunasInvisiveis: {
|
||||
type: PropType<Array<tipoEliColuna<any>>>;
|
||||
type: PropType<import("./types-eli-tabela").tipoEliColuna<any>[]>;
|
||||
required: true;
|
||||
};
|
||||
temColunasInvisiveis: {
|
||||
|
|
@ -469,7 +459,7 @@ declare const __VLS_export: import("vue").DefineComponent<import("vue").ExtractP
|
|||
required: true;
|
||||
};
|
||||
linhas: {
|
||||
type: PropType<Array<unknown>>;
|
||||
type: PropType<unknown[]>;
|
||||
required: true;
|
||||
};
|
||||
temAcoes: {
|
||||
|
|
@ -509,11 +499,11 @@ declare const __VLS_export: import("vue").DefineComponent<import("vue").ExtractP
|
|||
ChevronDown: import("@lucide/vue").LucideIcon;
|
||||
}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<import("vue").ExtractPropTypes<{
|
||||
colunas: {
|
||||
type: PropType<Array<tipoEliColuna<any>>>;
|
||||
type: PropType<import("./types-eli-tabela").tipoEliColuna<any>[]>;
|
||||
required: true;
|
||||
};
|
||||
colunasInvisiveis: {
|
||||
type: PropType<Array<tipoEliColuna<any>>>;
|
||||
type: PropType<import("./types-eli-tabela").tipoEliColuna<any>[]>;
|
||||
required: true;
|
||||
};
|
||||
temColunasInvisiveis: {
|
||||
|
|
@ -525,7 +515,7 @@ declare const __VLS_export: import("vue").DefineComponent<import("vue").ExtractP
|
|||
required: true;
|
||||
};
|
||||
linhas: {
|
||||
type: PropType<Array<unknown>>;
|
||||
type: PropType<unknown[]>;
|
||||
required: true;
|
||||
};
|
||||
temAcoes: {
|
||||
|
|
@ -588,22 +578,37 @@ declare const __VLS_export: import("vue").DefineComponent<import("vue").ExtractP
|
|||
/**
|
||||
* EliTabela
|
||||
* Componente de tabela consultável com busca, paginação, ordenação e ações por linha.
|
||||
* O setup() é orquestrado por `useEliTabela`, que compõe os seguintes sub-composables:
|
||||
* - useEliTabelaConsulta — carregamento, paginação, ordenação, busca
|
||||
* - useEliTabelaFiltrosAvancados — filtros avançados, modal e persistência
|
||||
* - useEliTabelaSelecao — seleção múltipla de linhas
|
||||
* - useEliTabelaColunas — visibilidade, ordem e modal de colunas
|
||||
* - useEliTabelaMenu — menu de ações por linha e posicionamento de popup
|
||||
*/
|
||||
/** Enum de códigos de resposta utilizado na consulta */
|
||||
?: string;
|
||||
icone
|
||||
/**
|
||||
* EliTabela
|
||||
* Componente de tabela consultável com busca, paginação, ordenação e ações por linha.
|
||||
* O setup() é orquestrado por `useEliTabela`, que compõe os seguintes sub-composables:
|
||||
* - useEliTabelaConsulta — carregamento, paginação, ordenação, busca
|
||||
* - useEliTabelaFiltrosAvancados — filtros avançados, modal e persistência
|
||||
* - useEliTabelaSelecao — seleção múltipla de linhas
|
||||
* - useEliTabelaColunas — visibilidade, ordem e modal de colunas
|
||||
* - useEliTabelaMenu — menu de ações por linha e posicionamento de popup
|
||||
*/
|
||||
/** Enum de códigos de resposta utilizado na consulta */
|
||||
?: import("@lucide/vue").LucideIcon;
|
||||
acao
|
||||
/**
|
||||
* EliTabela
|
||||
* Componente de tabela consultável com busca, paginação, ordenação e ações por linha.
|
||||
* O setup() é orquestrado por `useEliTabela`, que compõe os seguintes sub-composables:
|
||||
* - useEliTabelaConsulta — carregamento, paginação, ordenação, busca
|
||||
* - useEliTabelaFiltrosAvancados — filtros avançados, modal e persistência
|
||||
* - useEliTabelaSelecao — seleção múltipla de linhas
|
||||
* - useEliTabelaColunas — visibilidade, ordem e modal de colunas
|
||||
* - useEliTabelaMenu — menu de ações por linha e posicionamento de popup
|
||||
*/
|
||||
/** Enum de códigos de resposta utilizado na consulta */
|
||||
?: () => void;
|
||||
}[];
|
||||
} | {
|
||||
|
|
@ -623,7 +628,7 @@ declare const __VLS_export: import("vue").DefineComponent<import("vue").ExtractP
|
|||
required: true;
|
||||
};
|
||||
colunasInvisiveis: {
|
||||
type: PropType<Array<tipoEliColuna<any>>>;
|
||||
type: PropType<import("./types-eli-tabela").tipoEliColuna<any>[]>;
|
||||
required: true;
|
||||
};
|
||||
}>, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<import("vue").ExtractPropTypes<{
|
||||
|
|
@ -632,7 +637,7 @@ declare const __VLS_export: import("vue").DefineComponent<import("vue").ExtractP
|
|||
required: true;
|
||||
};
|
||||
colunasInvisiveis: {
|
||||
type: PropType<Array<tipoEliColuna<any>>>;
|
||||
type: PropType<import("./types-eli-tabela").tipoEliColuna<any>[]>;
|
||||
required: true;
|
||||
};
|
||||
}>> & Readonly<{}>, {}, {}, {
|
||||
|
|
@ -661,22 +666,37 @@ declare const __VLS_export: import("vue").DefineComponent<import("vue").ExtractP
|
|||
/**
|
||||
* EliTabela
|
||||
* Componente de tabela consultável com busca, paginação, ordenação e ações por linha.
|
||||
* O setup() é orquestrado por `useEliTabela`, que compõe os seguintes sub-composables:
|
||||
* - useEliTabelaConsulta — carregamento, paginação, ordenação, busca
|
||||
* - useEliTabelaFiltrosAvancados — filtros avançados, modal e persistência
|
||||
* - useEliTabelaSelecao — seleção múltipla de linhas
|
||||
* - useEliTabelaColunas — visibilidade, ordem e modal de colunas
|
||||
* - useEliTabelaMenu — menu de ações por linha e posicionamento de popup
|
||||
*/
|
||||
/** Enum de códigos de resposta utilizado na consulta */
|
||||
?: string;
|
||||
icone
|
||||
/**
|
||||
* EliTabela
|
||||
* Componente de tabela consultável com busca, paginação, ordenação e ações por linha.
|
||||
* O setup() é orquestrado por `useEliTabela`, que compõe os seguintes sub-composables:
|
||||
* - useEliTabelaConsulta — carregamento, paginação, ordenação, busca
|
||||
* - useEliTabelaFiltrosAvancados — filtros avançados, modal e persistência
|
||||
* - useEliTabelaSelecao — seleção múltipla de linhas
|
||||
* - useEliTabelaColunas — visibilidade, ordem e modal de colunas
|
||||
* - useEliTabelaMenu — menu de ações por linha e posicionamento de popup
|
||||
*/
|
||||
/** Enum de códigos de resposta utilizado na consulta */
|
||||
?: import("@lucide/vue").LucideIcon;
|
||||
acao
|
||||
/**
|
||||
* EliTabela
|
||||
* Componente de tabela consultável com busca, paginação, ordenação e ações por linha.
|
||||
* O setup() é orquestrado por `useEliTabela`, que compõe os seguintes sub-composables:
|
||||
* - useEliTabelaConsulta — carregamento, paginação, ordenação, busca
|
||||
* - useEliTabelaFiltrosAvancados — filtros avançados, modal e persistência
|
||||
* - useEliTabelaSelecao — seleção múltipla de linhas
|
||||
* - useEliTabelaColunas — visibilidade, ordem e modal de colunas
|
||||
* - useEliTabelaMenu — menu de ações por linha e posicionamento de popup
|
||||
*/
|
||||
/** Enum de códigos de resposta utilizado na consulta */
|
||||
?: () => void;
|
||||
}[];
|
||||
} | {
|
||||
|
|
@ -708,11 +728,11 @@ declare const __VLS_export: import("vue").DefineComponent<import("vue").ExtractP
|
|||
required: true;
|
||||
};
|
||||
acoes: {
|
||||
type: PropType<Array<{
|
||||
type: PropType<{
|
||||
acao: import("./types-eli-tabela").tipoEliTabelaAcao<any>;
|
||||
indice: number;
|
||||
visivel: boolean;
|
||||
}>>;
|
||||
}[]>;
|
||||
required: true;
|
||||
};
|
||||
linha: {
|
||||
|
|
@ -743,11 +763,11 @@ declare const __VLS_export: import("vue").DefineComponent<import("vue").ExtractP
|
|||
required: true;
|
||||
};
|
||||
acoes: {
|
||||
type: PropType<Array<{
|
||||
type: PropType<{
|
||||
acao: import("./types-eli-tabela").tipoEliTabelaAcao<any>;
|
||||
indice: number;
|
||||
visivel: boolean;
|
||||
}>>;
|
||||
}[]>;
|
||||
required: true;
|
||||
};
|
||||
linha: {
|
||||
|
|
@ -778,28 +798,10 @@ declare const __VLS_export: import("vue").DefineComponent<import("vue").ExtractP
|
|||
};
|
||||
acoes: {
|
||||
type: PropType<Array<{
|
||||
icone
|
||||
/**
|
||||
* EliTabela
|
||||
* Componente de tabela consultável com busca, paginação, ordenação e ações por linha.
|
||||
*/
|
||||
/** Enum de códigos de resposta utilizado na consulta */
|
||||
?: any;
|
||||
cor
|
||||
/**
|
||||
* EliTabela
|
||||
* Componente de tabela consultável com busca, paginação, ordenação e ações por linha.
|
||||
*/
|
||||
/** Enum de códigos de resposta utilizado na consulta */
|
||||
?: string;
|
||||
icone?: any;
|
||||
cor?: string;
|
||||
rotulo: string;
|
||||
acao: (params
|
||||
/**
|
||||
* EliTabela
|
||||
* Componente de tabela consultável com busca, paginação, ordenação e ações por linha.
|
||||
*/
|
||||
/** Enum de códigos de resposta utilizado na consulta */
|
||||
?: any) => void;
|
||||
acao: (params?: any) => void;
|
||||
}>>;
|
||||
required: false;
|
||||
default: () => never[];
|
||||
|
|
@ -837,28 +839,10 @@ declare const __VLS_export: import("vue").DefineComponent<import("vue").ExtractP
|
|||
};
|
||||
acoes: {
|
||||
type: PropType<Array<{
|
||||
icone
|
||||
/**
|
||||
* EliTabela
|
||||
* Componente de tabela consultável com busca, paginação, ordenação e ações por linha.
|
||||
*/
|
||||
/** Enum de códigos de resposta utilizado na consulta */
|
||||
?: any;
|
||||
cor
|
||||
/**
|
||||
* EliTabela
|
||||
* Componente de tabela consultável com busca, paginação, ordenação e ações por linha.
|
||||
*/
|
||||
/** Enum de códigos de resposta utilizado na consulta */
|
||||
?: string;
|
||||
icone?: any;
|
||||
cor?: string;
|
||||
rotulo: string;
|
||||
acao: (params
|
||||
/**
|
||||
* EliTabela
|
||||
* Componente de tabela consultável com busca, paginação, ordenação e ações por linha.
|
||||
*/
|
||||
/** Enum de códigos de resposta utilizado na consulta */
|
||||
?: any) => void;
|
||||
acao: (params?: any) => void;
|
||||
}>>;
|
||||
required: false;
|
||||
default: () => never[];
|
||||
|
|
@ -874,13 +858,7 @@ declare const __VLS_export: import("vue").DefineComponent<import("vue").ExtractP
|
|||
icone?: any;
|
||||
cor?: string;
|
||||
rotulo: string;
|
||||
acao: (params
|
||||
/**
|
||||
* EliTabela
|
||||
* Componente de tabela consultável com busca, paginação, ordenação e ações por linha.
|
||||
*/
|
||||
/** Enum de códigos de resposta utilizado na consulta */
|
||||
?: any) => void;
|
||||
acao: (params?: any) => void;
|
||||
}[];
|
||||
}, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
|
||||
EliTabelaModalColunas: import("vue").DefineComponent<import("vue").ExtractPropTypes<{
|
||||
|
|
@ -893,26 +871,26 @@ declare const __VLS_export: import("vue").DefineComponent<import("vue").ExtractP
|
|||
required: true;
|
||||
};
|
||||
configInicial: {
|
||||
type: PropType<EliTabelaColunasConfig>;
|
||||
type: PropType<import("./colunasStorage").EliTabelaColunasConfig>;
|
||||
required: true;
|
||||
};
|
||||
colunas: {
|
||||
type: PropType<Array<tipoEliColuna<any>>>;
|
||||
type: PropType<import("./types-eli-tabela").tipoEliColuna<any>[]>;
|
||||
required: true;
|
||||
};
|
||||
}>, {
|
||||
emitFechar: () => void;
|
||||
emitSalvar: () => void;
|
||||
visiveisLocal: import("vue").Ref<string[], string[]>;
|
||||
invisiveisLocal: import("vue").Ref<string[], string[]>;
|
||||
itemSendoArrastado: import("vue").Ref<string | null, string | null>;
|
||||
emitFechar: () => void;
|
||||
emitSalvar: () => void;
|
||||
onDragStart: (e: DragEvent, rotulo: string) => void;
|
||||
onDragEnd: () => void;
|
||||
onDragOverItem: (alvoRotulo: string, destino: "visiveis" | "invisiveis") => void;
|
||||
onDragOverLista: (destino: "visiveis" | "invisiveis") => void;
|
||||
}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
|
||||
fechar(): true;
|
||||
salvar(_config: EliTabelaColunasConfig): true;
|
||||
salvar(_config: import("./colunasStorage").EliTabelaColunasConfig): true;
|
||||
}, string, import("vue").PublicProps, Readonly<import("vue").ExtractPropTypes<{
|
||||
aberto: {
|
||||
type: BooleanConstructor;
|
||||
|
|
@ -923,35 +901,44 @@ declare const __VLS_export: import("vue").DefineComponent<import("vue").ExtractP
|
|||
required: true;
|
||||
};
|
||||
configInicial: {
|
||||
type: PropType<EliTabelaColunasConfig>;
|
||||
type: PropType<import("./colunasStorage").EliTabelaColunasConfig>;
|
||||
required: true;
|
||||
};
|
||||
colunas: {
|
||||
type: PropType<Array<tipoEliColuna<any>>>;
|
||||
type: PropType<import("./types-eli-tabela").tipoEliColuna<any>[]>;
|
||||
required: true;
|
||||
};
|
||||
}>> & Readonly<{
|
||||
onFechar?: (() => any) | undefined;
|
||||
onSalvar?: ((_config: EliTabelaColunasConfig) => any) | undefined;
|
||||
}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
|
||||
onSalvar?: ((_config: import("./colunasStorage").EliTabelaColunasConfig) => any) | undefined;
|
||||
}>, {}, {}, {
|
||||
Columns2: import("@lucide/vue").LucideIcon;
|
||||
Eye: import("@lucide/vue").LucideIcon;
|
||||
EyeOff: import("@lucide/vue").LucideIcon;
|
||||
GripVertical: import("@lucide/vue").LucideIcon;
|
||||
X: import("@lucide/vue").LucideIcon;
|
||||
Check: import("@lucide/vue").LucideIcon;
|
||||
}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
|
||||
EliTabelaModalFiltroAvancado: import("vue").DefineComponent<import("vue").ExtractPropTypes<{
|
||||
aberto: {
|
||||
type: BooleanConstructor;
|
||||
required: true;
|
||||
};
|
||||
filtrosBase: {
|
||||
type: PropType<Array<{
|
||||
chave: string;
|
||||
rotulo: string;
|
||||
filtro: (valor: unknown) => import("p-comuns").tipoFiltro26<any>;
|
||||
entrada: import("../EliEntrada/tiposEntradas").ComponenteEntrada;
|
||||
}>>;
|
||||
type: PropType<FiltroBase<string>[]>;
|
||||
required: true;
|
||||
};
|
||||
modelo: {
|
||||
type: PropType<Array<any>>;
|
||||
type: PropType<Array<{
|
||||
chave: string;
|
||||
valor: unknown;
|
||||
}>>;
|
||||
required: true;
|
||||
};
|
||||
modeloFormula: {
|
||||
type: StringConstructor;
|
||||
default: string;
|
||||
};
|
||||
}>, {
|
||||
linhas: import("vue").Ref<{
|
||||
chave: string;
|
||||
|
|
@ -1009,12 +996,8 @@ declare const __VLS_export: import("vue").DefineComponent<import("vue").ExtractP
|
|||
densidade?: import("../..").CampoDensidade | undefined;
|
||||
variante?: import("../..").CampoVariante | undefined;
|
||||
}];
|
||||
valor: any;
|
||||
}[], {
|
||||
chave: string;
|
||||
entrada: import("../EliEntrada/tiposEntradas").ComponenteEntrada;
|
||||
valor: any;
|
||||
}[] | {
|
||||
valor: unknown;
|
||||
}[], import("./useFiltroAvancado").LinhaFiltro[] | {
|
||||
chave: string;
|
||||
entrada: readonly ["texto", {
|
||||
rotulo: string;
|
||||
|
|
@ -1070,15 +1053,17 @@ declare const __VLS_export: import("vue").DefineComponent<import("vue").ExtractP
|
|||
densidade?: import("../..").CampoDensidade | undefined;
|
||||
variante?: import("../..").CampoVariante | undefined;
|
||||
}];
|
||||
valor: any;
|
||||
valor: unknown;
|
||||
}[]>;
|
||||
opcoesParaAdicionar: import("vue").ComputedRef<{
|
||||
chave: string;
|
||||
rotulo: string;
|
||||
filtro: (valor: unknown) => import("p-comuns").tipoFiltro26<any>;
|
||||
filtro: (valor: unknown) => import("p-comuns").tipoFiltroDeltaColunaTipada<"small" | "length" | "concat" | "slice" | "indexOf" | "lastIndexOf" | "includes" | "toString" | "charAt" | "charCodeAt" | "localeCompare" | "match" | "replace" | "search" | "split" | "substring" | "toLowerCase" | "toLocaleLowerCase" | "toUpperCase" | "toLocaleUpperCase" | "trim" | "substr" | "codePointAt" | "endsWith" | "normalize" | "repeat" | "startsWith" | "anchor" | "big" | "blink" | "bold" | "fixed" | "fontcolor" | "fontsize" | "italics" | "link" | "strike" | "sub" | "sup" | "padStart" | "padEnd" | "trimEnd" | "trimStart" | "trimLeft" | "trimRight" | "matchAll" | "valueOf"> | undefined;
|
||||
entrada: import("../EliEntrada/tiposEntradas").ComponenteEntrada;
|
||||
}[]>;
|
||||
chaveParaAdicionar: import("vue").Ref<string, string>;
|
||||
formulaCustomizada: import("vue").Ref<string, string>;
|
||||
formulaGerada: import("vue").ComputedRef<string>;
|
||||
componenteEntrada: (entrada: import("../EliEntrada/tiposEntradas").ComponenteEntrada) => import("vue").DefineComponent<import("vue").ExtractPropTypes<{
|
||||
value: {
|
||||
type: PropType<string | null | undefined>;
|
||||
|
|
@ -1418,43 +1403,57 @@ declare const __VLS_export: import("vue").DefineComponent<import("vue").ExtractP
|
|||
}>, {
|
||||
value: string | null | undefined;
|
||||
}, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
|
||||
opcoesEntrada: (entrada: import("../EliEntrada/tiposEntradas").ComponenteEntrada) => any;
|
||||
opcoesEntrada: (entrada: import("../EliEntrada/tiposEntradas").ComponenteEntrada) => {
|
||||
rotulo: string;
|
||||
};
|
||||
adicionar: () => void;
|
||||
remover: (idx: number) => void;
|
||||
emitFechar: () => void;
|
||||
emitSalvar: () => void;
|
||||
emitLimpar: () => void;
|
||||
rotuloDoFiltro: (f: {
|
||||
chave: string;
|
||||
rotulo: string;
|
||||
filtro: (valor: unknown) => import("p-comuns").tipoFiltro26<any>;
|
||||
entrada: import("../EliEntrada/tiposEntradas").ComponenteEntrada;
|
||||
}) => string;
|
||||
rotuloDoFiltro: typeof import("./useFiltroAvancado").rotuloDoFiltro;
|
||||
rotuloDoFiltroAtivo: (chave: string) => string;
|
||||
}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
|
||||
fechar: () => true;
|
||||
limpar: () => true;
|
||||
salvar: (_linhas: any[]) => true;
|
||||
salvar: (_linhas: Array<{
|
||||
chave: string;
|
||||
valor: unknown;
|
||||
}>, _formula?: string) => true;
|
||||
}, string, import("vue").PublicProps, Readonly<import("vue").ExtractPropTypes<{
|
||||
aberto: {
|
||||
type: BooleanConstructor;
|
||||
required: true;
|
||||
};
|
||||
filtrosBase: {
|
||||
type: PropType<Array<{
|
||||
chave: string;
|
||||
rotulo: string;
|
||||
filtro: (valor: unknown) => import("p-comuns").tipoFiltro26<any>;
|
||||
entrada: import("../EliEntrada/tiposEntradas").ComponenteEntrada;
|
||||
}>>;
|
||||
type: PropType<FiltroBase<string>[]>;
|
||||
required: true;
|
||||
};
|
||||
modelo: {
|
||||
type: PropType<Array<any>>;
|
||||
type: PropType<Array<{
|
||||
chave: string;
|
||||
valor: unknown;
|
||||
}>>;
|
||||
required: true;
|
||||
};
|
||||
modeloFormula: {
|
||||
type: StringConstructor;
|
||||
default: string;
|
||||
};
|
||||
}>> & Readonly<{
|
||||
onFechar?: (() => any) | undefined;
|
||||
onSalvar?: ((_linhas: any[]) => any) | undefined;
|
||||
onSalvar?: ((_linhas: {
|
||||
chave: string;
|
||||
valor: unknown;
|
||||
}[], _formula?: string | undefined) => any) | undefined;
|
||||
onLimpar?: (() => any) | undefined;
|
||||
}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
|
||||
}>, {
|
||||
modeloFormula: string;
|
||||
}, {}, {
|
||||
CircleHelp: import("@lucide/vue").LucideIcon;
|
||||
Filter: import("@lucide/vue").LucideIcon;
|
||||
Plus: import("@lucide/vue").LucideIcon;
|
||||
Trash2: import("@lucide/vue").LucideIcon;
|
||||
X: import("@lucide/vue").LucideIcon;
|
||||
}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
|
||||
}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
|
||||
|
|
|
|||
|
|
@ -4,11 +4,11 @@ declare const _default: typeof __VLS_export;
|
|||
export default _default;
|
||||
declare const __VLS_export: import("vue").DefineComponent<import("vue").ExtractPropTypes<{
|
||||
colunas: {
|
||||
type: PropType<Array<tipoEliColuna<any>>>;
|
||||
type: PropType<tipoEliColuna<any>[]>;
|
||||
required: true;
|
||||
};
|
||||
colunasInvisiveis: {
|
||||
type: PropType<Array<tipoEliColuna<any>>>;
|
||||
type: PropType<tipoEliColuna<any>[]>;
|
||||
required: true;
|
||||
};
|
||||
temColunasInvisiveis: {
|
||||
|
|
@ -20,7 +20,7 @@ declare const __VLS_export: import("vue").DefineComponent<import("vue").ExtractP
|
|||
required: true;
|
||||
};
|
||||
linhas: {
|
||||
type: PropType<Array<unknown>>;
|
||||
type: PropType<unknown[]>;
|
||||
required: true;
|
||||
};
|
||||
temAcoes: {
|
||||
|
|
@ -60,11 +60,11 @@ declare const __VLS_export: import("vue").DefineComponent<import("vue").ExtractP
|
|||
ChevronDown: import("@lucide/vue").LucideIcon;
|
||||
}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<import("vue").ExtractPropTypes<{
|
||||
colunas: {
|
||||
type: PropType<Array<tipoEliColuna<any>>>;
|
||||
type: PropType<tipoEliColuna<any>[]>;
|
||||
required: true;
|
||||
};
|
||||
colunasInvisiveis: {
|
||||
type: PropType<Array<tipoEliColuna<any>>>;
|
||||
type: PropType<tipoEliColuna<any>[]>;
|
||||
required: true;
|
||||
};
|
||||
temColunasInvisiveis: {
|
||||
|
|
@ -76,7 +76,7 @@ declare const __VLS_export: import("vue").DefineComponent<import("vue").ExtractP
|
|||
required: true;
|
||||
};
|
||||
linhas: {
|
||||
type: PropType<Array<unknown>>;
|
||||
type: PropType<unknown[]>;
|
||||
required: true;
|
||||
};
|
||||
temAcoes: {
|
||||
|
|
@ -156,7 +156,7 @@ declare const __VLS_export: import("vue").DefineComponent<import("vue").ExtractP
|
|||
required: true;
|
||||
};
|
||||
colunasInvisiveis: {
|
||||
type: PropType<Array<tipoEliColuna<any>>>;
|
||||
type: PropType<tipoEliColuna<any>[]>;
|
||||
required: true;
|
||||
};
|
||||
}>, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<import("vue").ExtractPropTypes<{
|
||||
|
|
@ -165,7 +165,7 @@ declare const __VLS_export: import("vue").DefineComponent<import("vue").ExtractP
|
|||
required: true;
|
||||
};
|
||||
colunasInvisiveis: {
|
||||
type: PropType<Array<tipoEliColuna<any>>>;
|
||||
type: PropType<tipoEliColuna<any>[]>;
|
||||
required: true;
|
||||
};
|
||||
}>> & Readonly<{}>, {}, {}, {
|
||||
|
|
|
|||
|
|
@ -82,6 +82,7 @@ declare const __VLS_export: import("vue").DefineComponent<import("vue").ExtractP
|
|||
exibirBotaoColunas: boolean;
|
||||
exibirBotaoFiltroAvancado: boolean;
|
||||
}, {}, {
|
||||
Columns3: import("@lucide/vue").LucideIcon;
|
||||
EliTabelaCaixaDeBusca: import("vue").DefineComponent<import("vue").ExtractPropTypes<{
|
||||
modelo: {
|
||||
type: StringConstructor;
|
||||
|
|
@ -106,4 +107,5 @@ declare const __VLS_export: import("vue").DefineComponent<import("vue").ExtractP
|
|||
}, {}, {
|
||||
Search: import("@lucide/vue").LucideIcon;
|
||||
}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
|
||||
Filter: import("@lucide/vue").LucideIcon;
|
||||
}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ declare const __VLS_export: import("vue").DefineComponent<import("vue").ExtractP
|
|||
}>;
|
||||
required: true;
|
||||
};
|
||||
}>, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<import("vue").ExtractPropTypes<{
|
||||
}>, void, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<import("vue").ExtractPropTypes<{
|
||||
isDev: {
|
||||
type: BooleanConstructor;
|
||||
required: true;
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ declare const __VLS_export: import("vue").DefineComponent<import("vue").ExtractP
|
|||
required: true;
|
||||
};
|
||||
colunasInvisiveis: {
|
||||
type: PropType<Array<tipoEliColuna<any>>>;
|
||||
type: PropType<tipoEliColuna<any>[]>;
|
||||
required: true;
|
||||
};
|
||||
}>, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<import("vue").ExtractPropTypes<{
|
||||
|
|
@ -17,7 +17,7 @@ declare const __VLS_export: import("vue").DefineComponent<import("vue").ExtractP
|
|||
required: true;
|
||||
};
|
||||
colunasInvisiveis: {
|
||||
type: PropType<Array<tipoEliColuna<any>>>;
|
||||
type: PropType<tipoEliColuna<any>[]>;
|
||||
required: true;
|
||||
};
|
||||
}>> & Readonly<{}>, {}, {}, {
|
||||
|
|
|
|||
|
|
@ -32,4 +32,8 @@ declare const __VLS_export: import("vue").DefineComponent<import("vue").ExtractP
|
|||
}>> & Readonly<{}>, {
|
||||
erro: string | null;
|
||||
mensagemVazio: string | undefined;
|
||||
}, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
|
||||
}, {}, {
|
||||
AlertCircle: import("@lucide/vue").LucideIcon;
|
||||
Inbox: import("@lucide/vue").LucideIcon;
|
||||
LoaderCircle: import("@lucide/vue").LucideIcon;
|
||||
}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ declare const _default: typeof __VLS_export;
|
|||
export default _default;
|
||||
declare const __VLS_export: import("vue").DefineComponent<import("vue").ExtractPropTypes<{
|
||||
colunas: {
|
||||
type: PropType<Array<tipoEliColuna<any>>>;
|
||||
type: PropType<tipoEliColuna<any>[]>;
|
||||
required: true;
|
||||
};
|
||||
temAcoes: {
|
||||
|
|
@ -38,14 +38,14 @@ declare const __VLS_export: import("vue").DefineComponent<import("vue").ExtractP
|
|||
}>, {
|
||||
ArrowUp: import("@lucide/vue").LucideIcon;
|
||||
ArrowDown: import("@lucide/vue").LucideIcon;
|
||||
isOrdenavel: (coluna: any) => boolean;
|
||||
isOrdenavel: (coluna: tipoEliColuna<unknown>) => boolean;
|
||||
emitAlternarOrdenacao: (chave: string) => void;
|
||||
}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
|
||||
alternarOrdenacao(chave: string): boolean;
|
||||
"alternar-selecao-todos": () => true;
|
||||
}, string, import("vue").PublicProps, Readonly<import("vue").ExtractPropTypes<{
|
||||
colunas: {
|
||||
type: PropType<Array<tipoEliColuna<any>>>;
|
||||
type: PropType<tipoEliColuna<any>[]>;
|
||||
required: true;
|
||||
};
|
||||
temAcoes: {
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ declare const __VLS_export: import("vue").DefineComponent<import("vue").ExtractP
|
|||
required: true;
|
||||
};
|
||||
acoes: {
|
||||
type: PropType<Array<ItemAcao<any>>>;
|
||||
type: PropType<ItemAcao<any>[]>;
|
||||
required: true;
|
||||
};
|
||||
linha: {
|
||||
|
|
@ -51,7 +51,7 @@ declare const __VLS_export: import("vue").DefineComponent<import("vue").ExtractP
|
|||
required: true;
|
||||
};
|
||||
acoes: {
|
||||
type: PropType<Array<ItemAcao<any>>>;
|
||||
type: PropType<ItemAcao<any>[]>;
|
||||
required: true;
|
||||
};
|
||||
linha: {
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import { type PropType } from "vue";
|
||||
import "./EliTabelaModalColunas.css";
|
||||
import type { EliTabelaColunasConfig } from "./colunasStorage";
|
||||
import type { tipoEliColuna } from "./types-eli-tabela";
|
||||
type OrigemLista = "visiveis" | "invisiveis";
|
||||
declare const _default: typeof __VLS_export;
|
||||
export default _default;
|
||||
declare const __VLS_export: import("vue").DefineComponent<import("vue").ExtractPropTypes<{
|
||||
|
|
@ -18,19 +18,19 @@ declare const __VLS_export: import("vue").DefineComponent<import("vue").ExtractP
|
|||
required: true;
|
||||
};
|
||||
colunas: {
|
||||
type: PropType<Array<tipoEliColuna<any>>>;
|
||||
type: PropType<tipoEliColuna<any>[]>;
|
||||
required: true;
|
||||
};
|
||||
}>, {
|
||||
emitFechar: () => void;
|
||||
emitSalvar: () => void;
|
||||
visiveisLocal: import("vue").Ref<string[], string[]>;
|
||||
invisiveisLocal: import("vue").Ref<string[], string[]>;
|
||||
itemSendoArrastado: import("vue").Ref<string | null, string | null>;
|
||||
emitFechar: () => void;
|
||||
emitSalvar: () => void;
|
||||
onDragStart: (e: DragEvent, rotulo: string) => void;
|
||||
onDragEnd: () => void;
|
||||
onDragOverItem: (alvoRotulo: string, destino: OrigemLista) => void;
|
||||
onDragOverLista: (destino: OrigemLista) => void;
|
||||
onDragOverItem: (alvoRotulo: string, destino: "visiveis" | "invisiveis") => void;
|
||||
onDragOverLista: (destino: "visiveis" | "invisiveis") => void;
|
||||
}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
|
||||
fechar(): true;
|
||||
salvar(_config: EliTabelaColunasConfig): true;
|
||||
|
|
@ -48,10 +48,17 @@ declare const __VLS_export: import("vue").DefineComponent<import("vue").ExtractP
|
|||
required: true;
|
||||
};
|
||||
colunas: {
|
||||
type: PropType<Array<tipoEliColuna<any>>>;
|
||||
type: PropType<tipoEliColuna<any>[]>;
|
||||
required: true;
|
||||
};
|
||||
}>> & Readonly<{
|
||||
onFechar?: (() => any) | undefined;
|
||||
onSalvar?: ((_config: EliTabelaColunasConfig) => any) | undefined;
|
||||
}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
|
||||
}>, {}, {}, {
|
||||
Columns2: import("@lucide/vue").LucideIcon;
|
||||
Eye: import("@lucide/vue").LucideIcon;
|
||||
EyeOff: import("@lucide/vue").LucideIcon;
|
||||
GripVertical: import("@lucide/vue").LucideIcon;
|
||||
X: import("@lucide/vue").LucideIcon;
|
||||
Check: import("@lucide/vue").LucideIcon;
|
||||
}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
|
||||
|
|
|
|||
|
|
@ -1,13 +1,7 @@
|
|||
import { type PropType } from "vue";
|
||||
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;
|
||||
valor: any;
|
||||
};
|
||||
declare function rotuloDoFiltro(f: FiltroBase<any>): string;
|
||||
import type { FiltroBase } from "./useFiltroAvancado";
|
||||
import { rotuloDoFiltro } from "./useFiltroAvancado";
|
||||
import "./EliTabelaModalFiltroAvancado.css";
|
||||
declare const _default: typeof __VLS_export;
|
||||
export default _default;
|
||||
declare const __VLS_export: import("vue").DefineComponent<import("vue").ExtractPropTypes<{
|
||||
|
|
@ -16,13 +10,21 @@ declare const __VLS_export: import("vue").DefineComponent<import("vue").ExtractP
|
|||
required: true;
|
||||
};
|
||||
filtrosBase: {
|
||||
type: PropType<Array<FiltroBase<any>>>;
|
||||
type: PropType<FiltroBase<string>[]>;
|
||||
required: true;
|
||||
};
|
||||
modelo: {
|
||||
type: PropType<Array<any>>;
|
||||
/** Linhas persistidas do pai — normalizadas internamente via normalizarModelo() */
|
||||
type: PropType<Array<{
|
||||
chave: string;
|
||||
valor: unknown;
|
||||
}>>;
|
||||
required: true;
|
||||
};
|
||||
modeloFormula: {
|
||||
type: StringConstructor;
|
||||
default: string;
|
||||
};
|
||||
}>, {
|
||||
linhas: import("vue").Ref<{
|
||||
chave: string;
|
||||
|
|
@ -80,8 +82,8 @@ declare const __VLS_export: import("vue").DefineComponent<import("vue").ExtractP
|
|||
densidade?: import("../..").CampoDensidade | undefined;
|
||||
variante?: import("../..").CampoVariante | undefined;
|
||||
}];
|
||||
valor: any;
|
||||
}[], LinhaFiltro[] | {
|
||||
valor: unknown;
|
||||
}[], import("./useFiltroAvancado").LinhaFiltro[] | {
|
||||
chave: string;
|
||||
entrada: readonly ["texto", {
|
||||
rotulo: string;
|
||||
|
|
@ -137,16 +139,18 @@ declare const __VLS_export: import("vue").DefineComponent<import("vue").ExtractP
|
|||
densidade?: import("../..").CampoDensidade | undefined;
|
||||
variante?: import("../..").CampoVariante | undefined;
|
||||
}];
|
||||
valor: any;
|
||||
valor: unknown;
|
||||
}[]>;
|
||||
opcoesParaAdicionar: import("vue").ComputedRef<{
|
||||
chave: string;
|
||||
rotulo: string;
|
||||
filtro: (valor: unknown) => import("p-comuns").tipoFiltro26<any>;
|
||||
entrada: ComponenteEntrada;
|
||||
filtro: (valor: unknown) => import("p-comuns").tipoFiltroDeltaColunaTipada<"small" | "length" | "concat" | "slice" | "indexOf" | "lastIndexOf" | "includes" | "toString" | "charAt" | "charCodeAt" | "localeCompare" | "match" | "replace" | "search" | "split" | "substring" | "toLowerCase" | "toLocaleLowerCase" | "toUpperCase" | "toLocaleUpperCase" | "trim" | "substr" | "codePointAt" | "endsWith" | "normalize" | "repeat" | "startsWith" | "anchor" | "big" | "blink" | "bold" | "fixed" | "fontcolor" | "fontsize" | "italics" | "link" | "strike" | "sub" | "sup" | "padStart" | "padEnd" | "trimEnd" | "trimStart" | "trimLeft" | "trimRight" | "matchAll" | "valueOf"> | undefined;
|
||||
entrada: import("../EliEntrada/tiposEntradas").ComponenteEntrada;
|
||||
}[]>;
|
||||
chaveParaAdicionar: import("vue").Ref<string, string>;
|
||||
componenteEntrada: (entrada: ComponenteEntrada) => import("vue").DefineComponent<import("vue").ExtractPropTypes<{
|
||||
formulaCustomizada: import("vue").Ref<string, string>;
|
||||
formulaGerada: import("vue").ComputedRef<string>;
|
||||
componenteEntrada: (entrada: import("../EliEntrada/tiposEntradas").ComponenteEntrada) => import("vue").DefineComponent<import("vue").ExtractPropTypes<{
|
||||
value: {
|
||||
type: PropType<string | null | undefined>;
|
||||
default: undefined;
|
||||
|
|
@ -485,32 +489,58 @@ declare const __VLS_export: import("vue").DefineComponent<import("vue").ExtractP
|
|||
}>, {
|
||||
value: string | null | undefined;
|
||||
}, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
|
||||
opcoesEntrada: (entrada: ComponenteEntrada) => any;
|
||||
opcoesEntrada: (entrada: import("../EliEntrada/tiposEntradas").ComponenteEntrada) => {
|
||||
rotulo: string;
|
||||
};
|
||||
adicionar: () => void;
|
||||
remover: (idx: number) => void;
|
||||
emitFechar: () => void;
|
||||
emitSalvar: () => void;
|
||||
emitLimpar: () => void;
|
||||
rotuloDoFiltro: typeof rotuloDoFiltro;
|
||||
rotuloDoFiltroAtivo: (chave: string) => string;
|
||||
}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
|
||||
fechar: () => true;
|
||||
limpar: () => true;
|
||||
salvar: (_linhas: any[]) => true;
|
||||
/** Dado serializável emitido pelo botão Aplicar */
|
||||
salvar: (_linhas: Array<{
|
||||
chave: string;
|
||||
valor: unknown;
|
||||
}>, _formula?: string) => true;
|
||||
}, string, import("vue").PublicProps, Readonly<import("vue").ExtractPropTypes<{
|
||||
aberto: {
|
||||
type: BooleanConstructor;
|
||||
required: true;
|
||||
};
|
||||
filtrosBase: {
|
||||
type: PropType<Array<FiltroBase<any>>>;
|
||||
type: PropType<FiltroBase<string>[]>;
|
||||
required: true;
|
||||
};
|
||||
modelo: {
|
||||
type: PropType<Array<any>>;
|
||||
/** Linhas persistidas do pai — normalizadas internamente via normalizarModelo() */
|
||||
type: PropType<Array<{
|
||||
chave: string;
|
||||
valor: unknown;
|
||||
}>>;
|
||||
required: true;
|
||||
};
|
||||
modeloFormula: {
|
||||
type: StringConstructor;
|
||||
default: string;
|
||||
};
|
||||
}>> & Readonly<{
|
||||
onFechar?: (() => any) | undefined;
|
||||
onSalvar?: ((_linhas: any[]) => any) | undefined;
|
||||
onSalvar?: ((_linhas: {
|
||||
chave: string;
|
||||
valor: unknown;
|
||||
}[], _formula?: string | undefined) => any) | undefined;
|
||||
onLimpar?: (() => any) | undefined;
|
||||
}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
|
||||
}>, {
|
||||
modeloFormula: string;
|
||||
}, {}, {
|
||||
CircleHelp: import("@lucide/vue").LucideIcon;
|
||||
Filter: import("@lucide/vue").LucideIcon;
|
||||
Plus: import("@lucide/vue").LucideIcon;
|
||||
Trash2: import("@lucide/vue").LucideIcon;
|
||||
X: import("@lucide/vue").LucideIcon;
|
||||
}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import { type PropType } from "vue";
|
||||
import "./EliTabelaPaginacao.css";
|
||||
declare const _default: typeof __VLS_export;
|
||||
export default _default;
|
||||
declare const __VLS_export: import("vue").DefineComponent<import("vue").ExtractPropTypes<{
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import "dayjs/locale/pt-br";
|
||||
import { type PropType } from "vue";
|
||||
import type { tiposTabelaCelulas } from "./tiposTabelaCelulas";
|
||||
declare const _default: typeof __VLS_export;
|
||||
|
|
@ -8,11 +9,6 @@ declare const __VLS_export: import("vue").DefineComponent<import("vue").ExtractP
|
|||
required: false;
|
||||
};
|
||||
}>, {
|
||||
dados: {
|
||||
valor: string;
|
||||
formato: "data" | "data_hora" | "relativo";
|
||||
acao?: () => void;
|
||||
} | undefined;
|
||||
textoData: import("vue").ComputedRef<string>;
|
||||
}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<import("vue").ExtractPropTypes<{
|
||||
dados: {
|
||||
|
|
|
|||
|
|
@ -7,12 +7,6 @@ declare const __VLS_export: import("vue").DefineComponent<import("vue").ExtractP
|
|||
type: PropType<tiposTabelaCelulas["numero"]>;
|
||||
};
|
||||
}>, {
|
||||
dados: {
|
||||
numero: number;
|
||||
prefixo?: string;
|
||||
sufixo?: string;
|
||||
acao?: () => void;
|
||||
} | undefined;
|
||||
textoNumero: import("vue").ComputedRef<string>;
|
||||
}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<import("vue").ExtractPropTypes<{
|
||||
dados: {
|
||||
|
|
|
|||
|
|
@ -7,16 +7,7 @@ declare const __VLS_export: import("vue").DefineComponent<import("vue").ExtractP
|
|||
type: PropType<tiposTabelaCelulas["tags"]>;
|
||||
required: false;
|
||||
};
|
||||
}>, {
|
||||
dados: {
|
||||
opcoes: {
|
||||
rotulo: string;
|
||||
cor?: string;
|
||||
icone?: import("@lucide/vue").LucideIcon;
|
||||
acao?: () => void;
|
||||
}[];
|
||||
} | undefined;
|
||||
}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<import("vue").ExtractPropTypes<{
|
||||
}>, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<import("vue").ExtractPropTypes<{
|
||||
dados: {
|
||||
type: PropType<tiposTabelaCelulas["tags"]>;
|
||||
required: false;
|
||||
|
|
|
|||
|
|
@ -6,12 +6,7 @@ declare const __VLS_export: import("vue").DefineComponent<import("vue").ExtractP
|
|||
dados: {
|
||||
type: PropType<tiposTabelaCelulas["textoSimples"]>;
|
||||
};
|
||||
}>, {
|
||||
dados: {
|
||||
texto: string;
|
||||
acao?: () => void;
|
||||
} | undefined;
|
||||
}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<import("vue").ExtractPropTypes<{
|
||||
}>, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<import("vue").ExtractPropTypes<{
|
||||
dados: {
|
||||
type: PropType<tiposTabelaCelulas["textoSimples"]>;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -6,12 +6,7 @@ declare const __VLS_export: import("vue").DefineComponent<import("vue").ExtractP
|
|||
dados: {
|
||||
type: PropType<tiposTabelaCelulas["textoTruncado"]>;
|
||||
};
|
||||
}>, {
|
||||
dados: {
|
||||
texto: string;
|
||||
acao?: () => void;
|
||||
} | undefined;
|
||||
}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<import("vue").ExtractPropTypes<{
|
||||
}>, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<import("vue").ExtractPropTypes<{
|
||||
dados: {
|
||||
type: PropType<tiposTabelaCelulas["textoTruncado"]>;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,14 +1,19 @@
|
|||
/**
|
||||
* Mapa (dispatch table) que associa cada `tipoTabelaCelula` ao seu componente Vue de renderização.
|
||||
* Usado por `EliTabelaCelula.vue` para resolver dinamicamente qual componente renderizar
|
||||
* a partir do tipo retornado por `tipoEliColuna.celula(linha)`.
|
||||
*
|
||||
* Adicionar um novo tipo de célula requer:
|
||||
* 1. Criar o componente `EliTabelaCelula<Tipo>.vue`
|
||||
* 2. Adicionar a chave em `tipoTabelaCelula` (tiposTabelaCelulas.ts)
|
||||
* 3. Registrar aqui no mapa
|
||||
*/
|
||||
export declare const registryTabelaCelulas: {
|
||||
readonly textoSimples: import("vue").DefineComponent<import("vue").ExtractPropTypes<{
|
||||
dados: {
|
||||
type: import("vue").PropType<import("./tiposTabelaCelulas").tiposTabelaCelulas["textoSimples"]>;
|
||||
};
|
||||
}>, {
|
||||
dados: {
|
||||
texto: string;
|
||||
acao?: () => void;
|
||||
} | undefined;
|
||||
}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<import("vue").ExtractPropTypes<{
|
||||
}>, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<import("vue").ExtractPropTypes<{
|
||||
dados: {
|
||||
type: import("vue").PropType<import("./tiposTabelaCelulas").tiposTabelaCelulas["textoSimples"]>;
|
||||
};
|
||||
|
|
@ -17,12 +22,7 @@ export declare const registryTabelaCelulas: {
|
|||
dados: {
|
||||
type: import("vue").PropType<import("./tiposTabelaCelulas").tiposTabelaCelulas["textoTruncado"]>;
|
||||
};
|
||||
}>, {
|
||||
dados: {
|
||||
texto: string;
|
||||
acao?: () => void;
|
||||
} | undefined;
|
||||
}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<import("vue").ExtractPropTypes<{
|
||||
}>, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<import("vue").ExtractPropTypes<{
|
||||
dados: {
|
||||
type: import("vue").PropType<import("./tiposTabelaCelulas").tiposTabelaCelulas["textoTruncado"]>;
|
||||
};
|
||||
|
|
@ -32,12 +32,6 @@ export declare const registryTabelaCelulas: {
|
|||
type: import("vue").PropType<import("./tiposTabelaCelulas").tiposTabelaCelulas["numero"]>;
|
||||
};
|
||||
}>, {
|
||||
dados: {
|
||||
numero: number;
|
||||
prefixo?: string;
|
||||
sufixo?: string;
|
||||
acao?: () => void;
|
||||
} | undefined;
|
||||
textoNumero: import("vue").ComputedRef<string>;
|
||||
}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<import("vue").ExtractPropTypes<{
|
||||
dados: {
|
||||
|
|
@ -49,16 +43,7 @@ export declare const registryTabelaCelulas: {
|
|||
type: import("vue").PropType<import("./tiposTabelaCelulas").tiposTabelaCelulas["tags"]>;
|
||||
required: false;
|
||||
};
|
||||
}>, {
|
||||
dados: {
|
||||
opcoes: {
|
||||
rotulo: string;
|
||||
cor?: string;
|
||||
icone?: import("@lucide/vue").LucideIcon;
|
||||
acao?: () => void;
|
||||
}[];
|
||||
} | undefined;
|
||||
}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<import("vue").ExtractPropTypes<{
|
||||
}>, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<import("vue").ExtractPropTypes<{
|
||||
dados: {
|
||||
type: import("vue").PropType<import("./tiposTabelaCelulas").tiposTabelaCelulas["tags"]>;
|
||||
required: false;
|
||||
|
|
@ -729,11 +714,6 @@ export declare const registryTabelaCelulas: {
|
|||
required: false;
|
||||
};
|
||||
}>, {
|
||||
dados: {
|
||||
valor: string;
|
||||
formato: "data" | "data_hora" | "relativo";
|
||||
acao?: () => void;
|
||||
} | undefined;
|
||||
textoData: import("vue").ComputedRef<string>;
|
||||
}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<import("vue").ExtractPropTypes<{
|
||||
dados: {
|
||||
|
|
|
|||
|
|
@ -1,10 +1,36 @@
|
|||
export type EliTabelaColunasConfig = {
|
||||
/** Rotulos das colunas visiveis (em ordem). */
|
||||
/** Rótulos das colunas visíveis (em ordem). */
|
||||
visiveis: string[];
|
||||
/** Rotulos das colunas invisiveis. */
|
||||
/** Rótulos das colunas invisíveis. */
|
||||
invisiveis: string[];
|
||||
};
|
||||
/**
|
||||
* Retorna a chave de `localStorage` utilizada para persistir a configuração de colunas de uma tabela.
|
||||
*
|
||||
* @param nomeTabela - Identificador único da tabela (campo `nome` de `tipoEliTabelaConsulta`)
|
||||
* @returns Chave no formato `eli:tabela:<nomeTabela>:colunas`
|
||||
*/
|
||||
export declare function storageKeyColunas(nomeTabela: string): string;
|
||||
/**
|
||||
* Carrega do `localStorage` a configuração de visibilidade e ordem das colunas de uma tabela.
|
||||
* Em caso de dado corrompido ou ausente, retorna configuração vazia (sem colunas fixadas).
|
||||
*
|
||||
* @param nomeTabela - Identificador único da tabela
|
||||
* @returns Configuração salva ou `{ visiveis: [], invisiveis: [] }` como fallback seguro
|
||||
*/
|
||||
export declare function carregarConfigColunas(nomeTabela: string): EliTabelaColunasConfig;
|
||||
/**
|
||||
* Persiste no `localStorage` a configuração de visibilidade e ordem das colunas.
|
||||
* Falhas de armazenamento (modo privado, storage cheio) são silenciadas.
|
||||
*
|
||||
* @param nomeTabela - Identificador único da tabela
|
||||
* @param config - Configuração com listas de colunas visíveis e invisíveis
|
||||
*/
|
||||
export declare function salvarConfigColunas(nomeTabela: string, config: EliTabelaColunasConfig): void;
|
||||
/**
|
||||
* Remove do `localStorage` a configuração de colunas salva para uma tabela.
|
||||
* Usado para restaurar o estado padrão de visibilidade definido nas props.
|
||||
*
|
||||
* @param nomeTabela - Identificador único da tabela
|
||||
*/
|
||||
export declare function limparConfigColunas(nomeTabela: string): void;
|
||||
|
|
|
|||
|
|
@ -1,7 +1,33 @@
|
|||
export type EliTabelaFiltroAvancadoSalvo<T> = Array<{
|
||||
coluna: keyof T;
|
||||
valor: T[keyof T];
|
||||
}>;
|
||||
export type EliTabelaFiltroAvancadoSalvo<T> = {
|
||||
linhas: Array<{
|
||||
chave: keyof T;
|
||||
/** Valor vindo de JSON.parse — tipo desconhecido intencionalmente; faça narrowing no consumidor. */
|
||||
valor: unknown;
|
||||
}>;
|
||||
formula?: string;
|
||||
};
|
||||
/**
|
||||
* Carrega do `localStorage` as linhas de filtro avançado salvas para uma tabela.
|
||||
* Trata dois formatos legados: array direto de filtros (v1) e objeto `{ linhas, formula }` (v2).
|
||||
* Em caso de dado ausente, corrompido ou formato desconhecido, retorna estado vazio.
|
||||
*
|
||||
* @param nomeTabela - Identificador único da tabela (campo `nome` de `tipoEliTabelaConsulta`)
|
||||
* @returns Estado de filtros salvo, ou `{ linhas: [], formula: '' }` como fallback seguro
|
||||
*/
|
||||
export declare function carregarFiltroAvancado<T>(nomeTabela: string): EliTabelaFiltroAvancadoSalvo<T>;
|
||||
/**
|
||||
* Persiste no `localStorage` o estado do filtro avançado de uma tabela.
|
||||
* Salva tanto as linhas de filtro quanto a fórmula booleana customizada.
|
||||
* Falhas de armazenamento são silenciadas para não interromper o fluxo do usuário.
|
||||
*
|
||||
* @param nomeTabela - Identificador único da tabela
|
||||
* @param filtros - Estado completo dos filtros a persistir
|
||||
*/
|
||||
export declare function salvarFiltroAvancado<T>(nomeTabela: string, filtros: EliTabelaFiltroAvancadoSalvo<T>): void;
|
||||
/**
|
||||
* Remove do `localStorage` os filtros avançados salvos para uma tabela.
|
||||
* Chamado ao executar "Limpar Filtros" no modal de filtro avançado.
|
||||
*
|
||||
* @param nomeTabela - Identificador único da tabela
|
||||
*/
|
||||
export declare function limparFiltroAvancado(nomeTabela: string): void;
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
import type { LucideIcon } from "@lucide/vue";
|
||||
import type { tipoFiltro26 } from "p-comuns";
|
||||
export type tipoFiltro<T> = tipoFiltro26<T>;
|
||||
import type { tipoFiltroDeltaColunaTipada } from "p-comuns";
|
||||
import type { tipoResposta } from "p-respostas";
|
||||
import type { ComponenteEntrada } from "../EliEntrada/tiposEntradas";
|
||||
import type { tiposTabelaCelulas, tipoTabelaCelula } from "./celulas/tiposTabelaCelulas";
|
||||
|
|
@ -11,6 +10,20 @@ export type tipoComponenteCelulaBase<T extends tipoTabelaCelula> = readonly [
|
|||
export type tipoComponenteCelula = {
|
||||
[K in tipoTabelaCelula]: tipoComponenteCelulaBase<K>;
|
||||
}[tipoTabelaCelula];
|
||||
/**
|
||||
* Cria uma tupla tipada `[tipo, dados]` que representa o conteúdo de uma célula da tabela.
|
||||
* Funciona como type-safe builder: o TypeScript infere o tipo exato de `dados` a partir de `tipo`,
|
||||
* garantindo que não seja possível passar dados incorretos para um tipo de célula.
|
||||
*
|
||||
* Uso esperado dentro de `tipoEliColuna.celula`:
|
||||
* ```ts
|
||||
* celula: (linha) => celulaTabela("textoSimples", { texto: linha.nome })
|
||||
* ```
|
||||
*
|
||||
* @param tipo - Identificador do tipo de célula (chave de `tiposTabelaCelulas`)
|
||||
* @param dados - Dados tipados para o tipo de célula escolhido
|
||||
* @returns Tupla `[tipo, dados]` consumida pelo `EliTabelaCelula.vue`
|
||||
*/
|
||||
export declare const celulaTabela: <T extends tipoTabelaCelula>(tipo: T, dados: tiposTabelaCelulas[T]) => tipoComponenteCelulaBase<T>;
|
||||
export type { tiposTabelaCelulas, tipoTabelaCelula };
|
||||
export type tipoEliColuna<T> = {
|
||||
|
|
@ -52,7 +65,7 @@ export type tipoEliTabelaAcao<T> = {
|
|||
exibir?: boolean | ((linha: T) => Promise<boolean> | boolean);
|
||||
};
|
||||
export type parametrosConsulta<T> = {
|
||||
filtros?: tipoFiltro26<T>[];
|
||||
filtros?: tipoFiltroDeltaColunaTipada<Extract<keyof T, string>>[];
|
||||
coluna_ordem?: keyof T;
|
||||
direcao_ordem?: "asc" | "desc";
|
||||
offSet?: number;
|
||||
|
|
@ -124,7 +137,7 @@ export type tipoEliTabelaConsulta<T> = {
|
|||
/** Rótulo exibido no select de adicionar filtro */
|
||||
rotulo: string;
|
||||
/** vai gerar filtro e mescla com os filtros já existentes */
|
||||
filtro: (valor: unknown) => tipoFiltro26<T>;
|
||||
filtro: (valor: unknown) => tipoFiltroDeltaColunaTipada<Extract<keyof T, string>> | undefined;
|
||||
/** componente de entrada que vai receber o valor para o filtro */
|
||||
entrada: ComponenteEntrada;
|
||||
}[];
|
||||
|
|
|
|||
42
dist/types/componentes/EliTabela/useColunasOrdenacao.d.ts
vendored
Normal file
42
dist/types/componentes/EliTabela/useColunasOrdenacao.d.ts
vendored
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
/**
|
||||
* useColunasOrdenacao
|
||||
* Composable que encapsula a lógica de drag-and-drop e sincronização de estado
|
||||
* do modal de configuração de colunas da EliTabela.
|
||||
*
|
||||
* Responsável por:
|
||||
* - Sincronizar colunas visíveis/invisíveis com a configuração persistida
|
||||
* - Aplicar ordem salva e adicionar novas colunas ao final
|
||||
* - Gerenciar eventos de arrastar-e-soltar entre as duas listas
|
||||
*
|
||||
* Extraído de `EliTabelaModalColunas.vue` para manter o componente abaixo de 250 linhas.
|
||||
*/
|
||||
import { type Ref } from "vue";
|
||||
import type { EliTabelaColunasConfig } from "./colunasStorage";
|
||||
import type { tipoEliColuna } from "./types-eli-tabela";
|
||||
type OrigemLista = "visiveis" | "invisiveis";
|
||||
interface UseColunasOrdenacaoOptions {
|
||||
aberto: Ref<boolean>;
|
||||
rotulosColunas: Ref<string[]>;
|
||||
configInicial: Ref<EliTabelaColunasConfig>;
|
||||
colunas: Ref<tipoEliColuna<any>[]>;
|
||||
}
|
||||
/**
|
||||
* Composable de drag-and-drop para reordenação e alternância de visibilidade de colunas.
|
||||
* Sincroniza automaticamente quando `aberto`, `rotulosColunas`, `configInicial` ou `colunas` mudam.
|
||||
*
|
||||
* @param options.aberto - Ref que controla se o modal está aberto (gatilho de sincronização)
|
||||
* @param options.rotulosColunas - Ref com todos os rótulos de colunas da tabela
|
||||
* @param options.configInicial - Ref com a configuração persistida (visíveis + invisíveis)
|
||||
* @param options.colunas - Ref com a definição completa das colunas (para ler `visivel: false` padrão)
|
||||
* @returns Estado reativo e handlers de drag-and-drop prontos para o template
|
||||
*/
|
||||
export declare function useColunasOrdenacao({ aberto, rotulosColunas, configInicial, colunas, }: UseColunasOrdenacaoOptions): {
|
||||
visiveisLocal: Ref<string[], string[]>;
|
||||
invisiveisLocal: Ref<string[], string[]>;
|
||||
itemSendoArrastado: Ref<string | null, string | null>;
|
||||
onDragStart: (e: DragEvent, rotulo: string) => void;
|
||||
onDragEnd: () => void;
|
||||
onDragOverItem: (alvoRotulo: string, destino: OrigemLista) => void;
|
||||
onDragOverLista: (destino: OrigemLista) => void;
|
||||
};
|
||||
export {};
|
||||
132
dist/types/componentes/EliTabela/useEliTabela.d.ts
vendored
Normal file
132
dist/types/componentes/EliTabela/useEliTabela.d.ts
vendored
Normal file
|
|
@ -0,0 +1,132 @@
|
|||
import type { tipoEliTabelaConsulta } from "./types-eli-tabela";
|
||||
type TabelaProps = tipoEliTabelaConsulta<any>;
|
||||
export declare function useEliTabela(props: {
|
||||
tabela: TabelaProps;
|
||||
selecionados: unknown[];
|
||||
esconderDebug: boolean;
|
||||
}, emit: (event: "update:selecionados", value: unknown[]) => void): {
|
||||
menuAberto: import("vue").Ref<number | null, number | null>;
|
||||
menuPopup: import("vue").Ref<{
|
||||
menuEl: HTMLElement | null;
|
||||
} | null, {
|
||||
menuEl: HTMLElement | null;
|
||||
} | {
|
||||
menuEl: HTMLElement | null;
|
||||
} | null>;
|
||||
menuPopupPos: import("vue").Ref<{
|
||||
top: number;
|
||||
left: number;
|
||||
}, {
|
||||
top: number;
|
||||
left: number;
|
||||
} | {
|
||||
top: number;
|
||||
left: number;
|
||||
}>;
|
||||
possuiAcoes: (i: number) => boolean;
|
||||
toggleMenu: (i: number, evento?: MouseEvent) => void;
|
||||
acoesDisponiveisPorLinha: (i: number) => {
|
||||
acao: import("./types-eli-tabela").tipoEliTabelaAcao<any>;
|
||||
indice: number;
|
||||
visivel: boolean;
|
||||
}[];
|
||||
paginaAtual: import("vue").Ref<number, number>;
|
||||
valorBusca: import("vue").Ref<string, string>;
|
||||
modalColunasAberto: import("vue").Ref<boolean, boolean>;
|
||||
configColunas: import("vue").Ref<{
|
||||
visiveis: string[];
|
||||
invisiveis: string[];
|
||||
}, import("./colunasStorage").EliTabelaColunasConfig | {
|
||||
visiveis: string[];
|
||||
invisiveis: string[];
|
||||
}>;
|
||||
linhasExpandidas: import("vue").Ref<Record<number, boolean>, Record<number, boolean>>;
|
||||
rotulosColunas: import("vue").ComputedRef<string[]>;
|
||||
colunasEfetivas: import("vue").ComputedRef<any[]>;
|
||||
colunasInvisiveisEfetivas: import("vue").ComputedRef<import("./types-eli-tabela").tipoEliColuna<any>[]>;
|
||||
temColunasInvisiveis: import("vue").ComputedRef<boolean>;
|
||||
totalColunas: import("vue").ComputedRef<number>;
|
||||
abrirModalColunas: () => void;
|
||||
fecharModalColunas: () => void;
|
||||
salvarModalColunas: (cfg: import("./colunasStorage").EliTabelaColunasConfig) => void;
|
||||
alternarLinhaExpandida: (indice: number) => void;
|
||||
mostrarSelecao: import("vue").ComputedRef<boolean>;
|
||||
isSelecionado: (linha: unknown) => boolean;
|
||||
alternarSelecao: (linha: unknown) => void;
|
||||
todosSelecionados: import("vue").ComputedRef<boolean>;
|
||||
algunsSelecionados: import("vue").ComputedRef<boolean>;
|
||||
alternarSelecaoTodos: (forcarMarcar?: boolean) => void;
|
||||
filtrosUi: import("vue").Ref<{
|
||||
chave: string;
|
||||
valor: unknown;
|
||||
}[], {
|
||||
chave: string;
|
||||
valor: unknown;
|
||||
}[] | {
|
||||
chave: string;
|
||||
valor: unknown;
|
||||
}[]>;
|
||||
filtrosFormula: import("vue").Ref<string, string>;
|
||||
modalFiltroAberto: import("vue").Ref<boolean, boolean>;
|
||||
exibirFiltroAvancado: import("vue").ComputedRef<boolean>;
|
||||
filtrosAvancadosAtivos: import("vue").ComputedRef<import("p-comuns").tipoFiltroDeltaColunaTipada<any>[]>;
|
||||
abrirModalFiltro: () => void;
|
||||
fecharModalFiltro: () => void;
|
||||
limparFiltrosAvancados: () => void;
|
||||
salvarFiltrosAvancados: (novoLinhas: {
|
||||
chave: string;
|
||||
valor: unknown;
|
||||
}[], novaFormula?: string) => void;
|
||||
isDev: boolean;
|
||||
tabela: import("vue").ComputedRef<tipoEliTabelaConsulta<any>>;
|
||||
carregando: import("vue").Ref<boolean, boolean>;
|
||||
erro: import("vue").Ref<string | null, string | null>;
|
||||
linhas: import("vue").Ref<unknown[], unknown[]>;
|
||||
linhasPaginadas: import("vue").ComputedRef<unknown[]>;
|
||||
quantidade: import("vue").Ref<number, number>;
|
||||
quantidadeFiltrada: import("vue").ComputedRef<number>;
|
||||
acoesVisiveis: import("vue").Ref<boolean[][], boolean[][]>;
|
||||
colunaOrdenacao: import("vue").Ref<string | null, string | null>;
|
||||
direcaoOrdenacao: import("vue").Ref<"desc" | "asc", "desc" | "asc">;
|
||||
registrosPorConsulta: import("vue").ComputedRef<number>;
|
||||
totalPaginas: import("vue").ComputedRef<number>;
|
||||
temAcoes: import("vue").ComputedRef<boolean>;
|
||||
exibirBusca: import("vue").ComputedRef<boolean>;
|
||||
acoesSuperiores: import("vue").ComputedRef<{
|
||||
posicao: "superior" | "inferior";
|
||||
icone?: import("@lucide/vue").LucideIcon;
|
||||
cor?: string;
|
||||
rotulo: string;
|
||||
acao: (parametrosConsulta: import("./types-eli-tabela").parametrosConsulta<any> & {
|
||||
atualizarConsulta: () => Promise<void>;
|
||||
editarLista: (lista: any[]) => Promise<any[]>;
|
||||
}) => void;
|
||||
}[]>;
|
||||
acoesInferiores: import("vue").ComputedRef<{
|
||||
posicao: "superior" | "inferior";
|
||||
icone?: import("@lucide/vue").LucideIcon;
|
||||
cor?: string;
|
||||
rotulo: string;
|
||||
acao: (parametrosConsulta: import("./types-eli-tabela").parametrosConsulta<any> & {
|
||||
atualizarConsulta: () => Promise<void>;
|
||||
editarLista: (lista: any[]) => Promise<any[]>;
|
||||
}) => void;
|
||||
}[]>;
|
||||
temAcoesSuperiores: import("vue").ComputedRef<boolean>;
|
||||
temAcoesInferiores: import("vue").ComputedRef<boolean>;
|
||||
parametrosConsultaAtuais: import("vue").ComputedRef<{
|
||||
atualizarConsulta: () => Promise<void>;
|
||||
editarLista: (novaLista: any[]) => Promise<any[]>;
|
||||
filtros?: import("p-comuns").tipoFiltroDeltaColunaTipada<string>[] | undefined;
|
||||
coluna_ordem?: string | number | symbol | undefined;
|
||||
direcao_ordem?: "asc" | "desc";
|
||||
offSet?: number;
|
||||
limit?: number;
|
||||
texto_busca?: string;
|
||||
}>;
|
||||
carregar: () => Promise<void>;
|
||||
alternarOrdenacao: (chave?: string) => void;
|
||||
atualizarBusca: (texto: string) => void;
|
||||
irParaPagina: (pagina: number) => void;
|
||||
};
|
||||
export {};
|
||||
38
dist/types/componentes/EliTabela/useEliTabelaColunas.d.ts
vendored
Normal file
38
dist/types/componentes/EliTabela/useEliTabelaColunas.d.ts
vendored
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
/**
|
||||
* useEliTabelaColunas
|
||||
* Gerencia visibilidade, ordenação e persistência da configuração de colunas da EliTabela.
|
||||
* Controla também o modal de colunas e o estado de linhas expandidas (detalhe de colunas invisíveis).
|
||||
*
|
||||
* @param props - Props do componente (tabela.colunas, tabela.nome)
|
||||
* @param temAcoes - Computed que indica se existem ações por linha (para totalColunas)
|
||||
*/
|
||||
import { type Ref } from "vue";
|
||||
import { type EliTabelaColunasConfig } from "./colunasStorage";
|
||||
import type { tipoEliColuna } from "./types-eli-tabela";
|
||||
type PropsColunas = {
|
||||
tabela: {
|
||||
colunas: tipoEliColuna<any>[];
|
||||
nome: string;
|
||||
};
|
||||
};
|
||||
export declare function useEliTabelaColunas(props: PropsColunas, temAcoes: Ref<boolean>): {
|
||||
modalColunasAberto: Ref<boolean, boolean>;
|
||||
configColunas: Ref<{
|
||||
visiveis: string[];
|
||||
invisiveis: string[];
|
||||
}, EliTabelaColunasConfig | {
|
||||
visiveis: string[];
|
||||
invisiveis: string[];
|
||||
}>;
|
||||
linhasExpandidas: Ref<Record<number, boolean>, Record<number, boolean>>;
|
||||
rotulosColunas: import("vue").ComputedRef<string[]>;
|
||||
colunasEfetivas: import("vue").ComputedRef<any[]>;
|
||||
colunasInvisiveisEfetivas: import("vue").ComputedRef<tipoEliColuna<any>[]>;
|
||||
temColunasInvisiveis: import("vue").ComputedRef<boolean>;
|
||||
totalColunas: import("vue").ComputedRef<number>;
|
||||
abrirModalColunas: () => void;
|
||||
fecharModalColunas: () => void;
|
||||
salvarModalColunas: (cfg: EliTabelaColunasConfig) => void;
|
||||
alternarLinhaExpandida: (indice: number) => void;
|
||||
};
|
||||
export {};
|
||||
72
dist/types/componentes/EliTabela/useEliTabelaConsulta.d.ts
vendored
Normal file
72
dist/types/componentes/EliTabela/useEliTabelaConsulta.d.ts
vendored
Normal file
|
|
@ -0,0 +1,72 @@
|
|||
/**
|
||||
* useEliTabelaConsulta
|
||||
* Gerencia o ciclo de consultas da EliTabela: carregamento, paginação, ordenação e busca.
|
||||
* Usa um contador sequencial para descartar respostas de consultas antigas (race condition).
|
||||
*
|
||||
* @param props - Props do componente (tabela, esconderDebug)
|
||||
* @param filtrosAvancadosAtivos - Computed com filtros ativos para envio à API
|
||||
* @param paginaAtual - Ref da página atual (compartilhada com filtros e busca)
|
||||
* @param valorBusca - Ref da busca textual (compartilhada com filtros avançados)
|
||||
* @param linhasExpandidas - Ref das linhas expandidas (resetada ao carregar)
|
||||
*/
|
||||
import type { tipoFiltroDeltaColunaTipada } from "p-comuns";
|
||||
import { type Ref } from "vue";
|
||||
import type { parametrosConsulta, tipoEliTabelaConsulta } from "./types-eli-tabela";
|
||||
type PropsConsulta = {
|
||||
tabela: tipoEliTabelaConsulta<any>;
|
||||
esconderDebug: boolean;
|
||||
};
|
||||
export declare function useEliTabelaConsulta(props: PropsConsulta, filtrosAvancadosAtivos: Ref<tipoFiltroDeltaColunaTipada<any>[]>, paginaAtual: Ref<number>, valorBusca: Ref<string>, linhasExpandidas: Ref<Record<number, boolean>>): {
|
||||
isDev: boolean;
|
||||
tabela: import("vue").ComputedRef<tipoEliTabelaConsulta<any>>;
|
||||
carregando: Ref<boolean, boolean>;
|
||||
erro: Ref<string | null, string | null>;
|
||||
linhas: Ref<unknown[], unknown[]>;
|
||||
linhasPaginadas: import("vue").ComputedRef<unknown[]>;
|
||||
quantidade: Ref<number, number>;
|
||||
quantidadeFiltrada: import("vue").ComputedRef<number>;
|
||||
acoesVisiveis: Ref<boolean[][], boolean[][]>;
|
||||
colunaOrdenacao: Ref<string | null, string | null>;
|
||||
direcaoOrdenacao: Ref<"desc" | "asc", "desc" | "asc">;
|
||||
registrosPorConsulta: import("vue").ComputedRef<number>;
|
||||
totalPaginas: import("vue").ComputedRef<number>;
|
||||
temAcoes: import("vue").ComputedRef<boolean>;
|
||||
exibirBusca: import("vue").ComputedRef<boolean>;
|
||||
acoesSuperiores: import("vue").ComputedRef<{
|
||||
posicao: "superior" | "inferior";
|
||||
icone?: import("@lucide/vue").LucideIcon;
|
||||
cor?: string;
|
||||
rotulo: string;
|
||||
acao: (parametrosConsulta: parametrosConsulta<any> & {
|
||||
atualizarConsulta: () => Promise<void>;
|
||||
editarLista: (lista: any[]) => Promise<any[]>;
|
||||
}) => void;
|
||||
}[]>;
|
||||
acoesInferiores: import("vue").ComputedRef<{
|
||||
posicao: "superior" | "inferior";
|
||||
icone?: import("@lucide/vue").LucideIcon;
|
||||
cor?: string;
|
||||
rotulo: string;
|
||||
acao: (parametrosConsulta: parametrosConsulta<any> & {
|
||||
atualizarConsulta: () => Promise<void>;
|
||||
editarLista: (lista: any[]) => Promise<any[]>;
|
||||
}) => void;
|
||||
}[]>;
|
||||
temAcoesSuperiores: import("vue").ComputedRef<boolean>;
|
||||
temAcoesInferiores: import("vue").ComputedRef<boolean>;
|
||||
parametrosConsultaAtuais: import("vue").ComputedRef<{
|
||||
atualizarConsulta: () => Promise<void>;
|
||||
editarLista: (novaLista: any[]) => Promise<any[]>;
|
||||
filtros?: tipoFiltroDeltaColunaTipada<string>[] | undefined;
|
||||
coluna_ordem?: string | number | symbol | undefined;
|
||||
direcao_ordem?: "asc" | "desc";
|
||||
offSet?: number;
|
||||
limit?: number;
|
||||
texto_busca?: string;
|
||||
}>;
|
||||
carregar: () => Promise<void>;
|
||||
alternarOrdenacao: (chave?: string) => void;
|
||||
atualizarBusca: (texto: string) => void;
|
||||
irParaPagina: (pagina: number) => void;
|
||||
};
|
||||
export {};
|
||||
45
dist/types/componentes/EliTabela/useEliTabelaFiltrosAvancados.d.ts
vendored
Normal file
45
dist/types/componentes/EliTabela/useEliTabelaFiltrosAvancados.d.ts
vendored
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
/**
|
||||
* useEliTabelaFiltrosAvancados
|
||||
* Gerencia o estado de filtros avançados da EliTabela, incluindo:
|
||||
* - Linhas de filtro ativas (`filtrosUi`) e fórmula booleana (`filtrosFormula`)
|
||||
* - Computed `filtrosAvancadosAtivos` que converte as linhas em FiltroDelta para a API
|
||||
* - Abertura/fechamento e limpeza do modal de filtro avançado
|
||||
*
|
||||
* A fórmula é armazenada no formato `{chave}` (ex: `{email} E {nome}`) e convertida
|
||||
* para IDs globais `{c1} {c2}` durante a montagem do FiltroDelta final.
|
||||
*
|
||||
* @param props - Props do componente (tabela.filtroAvancado, tabela.nome)
|
||||
* @param paginaAtual - Ref da página atual (para resetar ao aplicar filtros)
|
||||
* @param valorBusca - Ref da busca textual (limpa ao aplicar filtros avançados)
|
||||
* @param carregar - Função de recarregamento dos dados
|
||||
*/
|
||||
import type { tipoFiltroDeltaColunaTipada } from "p-comuns";
|
||||
import { type Ref } from "vue";
|
||||
type PropsFiltros = {
|
||||
tabela: {
|
||||
filtroAvancado?: any[];
|
||||
nome: string;
|
||||
};
|
||||
};
|
||||
type LinhaFiltroUI = {
|
||||
chave: string;
|
||||
valor: unknown;
|
||||
};
|
||||
export declare function useEliTabelaFiltrosAvancados(props: PropsFiltros, paginaAtual: Ref<number>, valorBusca: Ref<string>, carregar: () => Promise<void>): {
|
||||
filtrosUi: Ref<{
|
||||
chave: string;
|
||||
valor: unknown;
|
||||
}[], LinhaFiltroUI[] | {
|
||||
chave: string;
|
||||
valor: unknown;
|
||||
}[]>;
|
||||
filtrosFormula: Ref<string, string>;
|
||||
modalFiltroAberto: Ref<boolean, boolean>;
|
||||
exibirFiltroAvancado: import("vue").ComputedRef<boolean>;
|
||||
filtrosAvancadosAtivos: import("vue").ComputedRef<tipoFiltroDeltaColunaTipada<any>[]>;
|
||||
abrirModalFiltro: () => void;
|
||||
fecharModalFiltro: () => void;
|
||||
limparFiltrosAvancados: () => void;
|
||||
salvarFiltrosAvancados: (novoLinhas: LinhaFiltroUI[], novaFormula?: string) => void;
|
||||
};
|
||||
export {};
|
||||
43
dist/types/componentes/EliTabela/useEliTabelaMenu.d.ts
vendored
Normal file
43
dist/types/componentes/EliTabela/useEliTabelaMenu.d.ts
vendored
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
/**
|
||||
* useEliTabelaMenu
|
||||
* Gerencia o menu de ações por linha da EliTabela.
|
||||
* Controla abertura/fechamento, posicionamento do popup e detecção de clique externo.
|
||||
*
|
||||
* @param props - Props do componente (tabela.acoesLinha)
|
||||
* @param acoesVisiveis - Ref com matriz de visibilidade das ações por linha
|
||||
*/
|
||||
import { type Ref } from "vue";
|
||||
import type { tipoEliTabelaAcao } from "./types-eli-tabela";
|
||||
type PropsMenu = {
|
||||
tabela: {
|
||||
acoesLinha?: tipoEliTabelaAcao<any>[];
|
||||
};
|
||||
};
|
||||
export declare function useEliTabelaMenu(props: PropsMenu, acoesVisiveis: Ref<boolean[][]>): {
|
||||
menuAberto: Ref<number | null, number | null>;
|
||||
menuPopup: Ref<{
|
||||
menuEl: HTMLElement | null;
|
||||
} | null, {
|
||||
menuEl: HTMLElement | null;
|
||||
} | {
|
||||
menuEl: HTMLElement | null;
|
||||
} | null>;
|
||||
menuPopupPos: Ref<{
|
||||
top: number;
|
||||
left: number;
|
||||
}, {
|
||||
top: number;
|
||||
left: number;
|
||||
} | {
|
||||
top: number;
|
||||
left: number;
|
||||
}>;
|
||||
possuiAcoes: (i: number) => boolean;
|
||||
toggleMenu: (i: number, evento?: MouseEvent) => void;
|
||||
acoesDisponiveisPorLinha: (i: number) => {
|
||||
acao: tipoEliTabelaAcao<any>;
|
||||
indice: number;
|
||||
visivel: boolean;
|
||||
}[];
|
||||
};
|
||||
export {};
|
||||
26
dist/types/componentes/EliTabela/useEliTabelaSelecao.d.ts
vendored
Normal file
26
dist/types/componentes/EliTabela/useEliTabelaSelecao.d.ts
vendored
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
/**
|
||||
* useEliTabelaSelecao
|
||||
* Gerencia a seleção múltipla de linhas na EliTabela.
|
||||
* Suporta seleção por objeto completo ou por chave configurável (`chave_selecao`).
|
||||
*
|
||||
* @param props - Props do componente (selecionados, tabela.chave_selecao, tabela.mostrar_selecao)
|
||||
* @param emit - Função de emit para `update:selecionados`
|
||||
* @param linhasPaginadas - Ref com as linhas da página atual (para todosSelecionados/algunsSelecionados)
|
||||
*/
|
||||
import { type Ref } from "vue";
|
||||
type PropsSelecao = {
|
||||
selecionados: unknown[];
|
||||
tabela: {
|
||||
mostrar_selecao?: boolean;
|
||||
chave_selecao?: string | number | symbol;
|
||||
};
|
||||
};
|
||||
export declare function useEliTabelaSelecao(props: PropsSelecao, emit: (event: "update:selecionados", value: unknown[]) => void, linhasPaginadas: Ref<unknown[]>): {
|
||||
mostrarSelecao: import("vue").ComputedRef<boolean>;
|
||||
isSelecionado: (linha: unknown) => boolean;
|
||||
alternarSelecao: (linha: unknown) => void;
|
||||
todosSelecionados: import("vue").ComputedRef<boolean>;
|
||||
algunsSelecionados: import("vue").ComputedRef<boolean>;
|
||||
alternarSelecaoTodos: (forcarMarcar?: boolean) => void;
|
||||
};
|
||||
export {};
|
||||
534
dist/types/componentes/EliTabela/useFiltroAvancado.d.ts
vendored
Normal file
534
dist/types/componentes/EliTabela/useFiltroAvancado.d.ts
vendored
Normal file
|
|
@ -0,0 +1,534 @@
|
|||
/**
|
||||
* useFiltroAvancado
|
||||
* Composable que encapsula a lógica do modal de filtro avançado da EliTabela.
|
||||
*
|
||||
* Responsável por:
|
||||
* - Gerenciar a lista de linhas de filtro ativas
|
||||
* - Converter fórmulas booleanas entre notação de exibição (`{c1}`) e persistência (`{chave}`)
|
||||
* - Normalizar filtros carregados do localStorage (compatibilidade com formatos legados)
|
||||
* - Resolver o componente de entrada correto por tipo (`texto`, `numero`, `dataHora`)
|
||||
*
|
||||
* Extraído de `EliTabelaModalFiltroAvancado.vue` para manter o componente abaixo de 250 linhas.
|
||||
*/
|
||||
import { type Ref } from "vue";
|
||||
import type { ComponenteEntrada } from "../EliEntrada/tiposEntradas";
|
||||
import type { tipoEliTabelaConsulta } from "./types-eli-tabela";
|
||||
export type FiltroBase<T> = NonNullable<tipoEliTabelaConsulta<T>["filtroAvancado"]>[number];
|
||||
export type LinhaFiltro = {
|
||||
chave: string;
|
||||
entrada: ComponenteEntrada;
|
||||
/** Valor do campo de filtro — unknown intencionalmente (vem de localStorage ou entrada do usuário). */
|
||||
valor: unknown;
|
||||
};
|
||||
/**
|
||||
* Retorna o rótulo de exibição de uma definição de filtro.
|
||||
* Utilizado no template do modal para exibir o nome do campo ao usuário.
|
||||
*
|
||||
* @param f - Definição de filtro da configuração da tabela
|
||||
* @returns Rótulo textual do filtro
|
||||
*/
|
||||
export declare function rotuloDoFiltro(f: FiltroBase<string>): string;
|
||||
interface UseFiltroAvancadoOptions {
|
||||
aberto: Ref<boolean>;
|
||||
filtrosBase: Ref<FiltroBase<string>[]>;
|
||||
modelo: Ref<Array<{
|
||||
chave: string;
|
||||
valor: unknown;
|
||||
}>>;
|
||||
modeloFormula: Ref<string>;
|
||||
emit: {
|
||||
(event: "fechar"): void;
|
||||
(event: "limpar"): void;
|
||||
(event: "salvar", linhas: Array<{
|
||||
chave: string;
|
||||
valor: unknown;
|
||||
}>, formula?: string): void;
|
||||
};
|
||||
}
|
||||
/**
|
||||
* Composable principal do modal de filtro avançado da EliTabela.
|
||||
* Sincroniza automaticamente quando o modal abre ou o modelo/filtrosBase mudam.
|
||||
*
|
||||
* @param options.aberto - Ref que controla se o modal está aberto
|
||||
* @param options.filtrosBase - Ref com as definições de filtros disponíveis na tabela
|
||||
* @param options.modelo - Ref com as linhas de filtro persistidas (vindo do pai)
|
||||
* @param options.modeloFormula - Ref com a fórmula booleana persistida no pai
|
||||
* @param options.emit - Função de emit do componente Vue pai
|
||||
* @returns Estado reativo e ações do modal de filtro
|
||||
*/
|
||||
export declare function useFiltroAvancado({ aberto, filtrosBase, modelo, modeloFormula, emit, }: UseFiltroAvancadoOptions): {
|
||||
linhas: Ref<{
|
||||
chave: string;
|
||||
entrada: readonly ["texto", {
|
||||
rotulo: string;
|
||||
placeholder?: string | undefined;
|
||||
limiteCaracteres?: number | undefined;
|
||||
formato?: "texto" | "email" | "url" | "telefone" | "cpfCnpj" | "cep" | undefined;
|
||||
}] | readonly ["dataHora", {
|
||||
rotulo: string;
|
||||
placeholder?: string | undefined;
|
||||
modo?: "data" | "dataHora" | undefined;
|
||||
limpavel?: boolean | undefined;
|
||||
erro?: boolean | undefined;
|
||||
mensagensErro?: string | string[] | undefined;
|
||||
dica?: string | undefined;
|
||||
dicaPersistente?: boolean | undefined;
|
||||
min?: string | undefined;
|
||||
max?: string | undefined;
|
||||
densidade?: import("../..").CampoDensidade | undefined;
|
||||
variante?: import("../..").CampoVariante | undefined;
|
||||
}] | readonly ["numero", {
|
||||
rotulo: string;
|
||||
placeholder?: string | undefined;
|
||||
sufixo?: string | undefined;
|
||||
prefixo?: string | undefined;
|
||||
precisao?: number | undefined;
|
||||
}] | readonly ["paragrafo", {
|
||||
rotulo: string;
|
||||
placeholder?: string | undefined;
|
||||
linhas?: number | undefined;
|
||||
limiteCaracteres?: number | undefined;
|
||||
limpavel?: boolean | undefined;
|
||||
erro?: boolean | undefined;
|
||||
mensagensErro?: string | string[] | undefined;
|
||||
dica?: string | undefined;
|
||||
dicaPersistente?: boolean | undefined;
|
||||
densidade?: import("../..").CampoDensidade | undefined;
|
||||
variante?: import("../..").CampoVariante | undefined;
|
||||
}] | readonly ["selecao", {
|
||||
rotulo: string;
|
||||
placeholder?: string | undefined;
|
||||
itens: () => {
|
||||
chave: string;
|
||||
rotulo: string;
|
||||
}[] | Promise<{
|
||||
chave: string;
|
||||
rotulo: string;
|
||||
}[]>;
|
||||
limpavel?: boolean | undefined;
|
||||
erro?: boolean | undefined;
|
||||
mensagensErro?: string | string[] | undefined;
|
||||
dica?: string | undefined;
|
||||
dicaPersistente?: boolean | undefined;
|
||||
densidade?: import("../..").CampoDensidade | undefined;
|
||||
variante?: import("../..").CampoVariante | undefined;
|
||||
}];
|
||||
valor: unknown;
|
||||
}[], LinhaFiltro[] | {
|
||||
chave: string;
|
||||
entrada: readonly ["texto", {
|
||||
rotulo: string;
|
||||
placeholder?: string | undefined;
|
||||
limiteCaracteres?: number | undefined;
|
||||
formato?: "texto" | "email" | "url" | "telefone" | "cpfCnpj" | "cep" | undefined;
|
||||
}] | readonly ["dataHora", {
|
||||
rotulo: string;
|
||||
placeholder?: string | undefined;
|
||||
modo?: "data" | "dataHora" | undefined;
|
||||
limpavel?: boolean | undefined;
|
||||
erro?: boolean | undefined;
|
||||
mensagensErro?: string | string[] | undefined;
|
||||
dica?: string | undefined;
|
||||
dicaPersistente?: boolean | undefined;
|
||||
min?: string | undefined;
|
||||
max?: string | undefined;
|
||||
densidade?: import("../..").CampoDensidade | undefined;
|
||||
variante?: import("../..").CampoVariante | undefined;
|
||||
}] | readonly ["numero", {
|
||||
rotulo: string;
|
||||
placeholder?: string | undefined;
|
||||
sufixo?: string | undefined;
|
||||
prefixo?: string | undefined;
|
||||
precisao?: number | undefined;
|
||||
}] | readonly ["paragrafo", {
|
||||
rotulo: string;
|
||||
placeholder?: string | undefined;
|
||||
linhas?: number | undefined;
|
||||
limiteCaracteres?: number | undefined;
|
||||
limpavel?: boolean | undefined;
|
||||
erro?: boolean | undefined;
|
||||
mensagensErro?: string | string[] | undefined;
|
||||
dica?: string | undefined;
|
||||
dicaPersistente?: boolean | undefined;
|
||||
densidade?: import("../..").CampoDensidade | undefined;
|
||||
variante?: import("../..").CampoVariante | undefined;
|
||||
}] | readonly ["selecao", {
|
||||
rotulo: string;
|
||||
placeholder?: string | undefined;
|
||||
itens: () => {
|
||||
chave: string;
|
||||
rotulo: string;
|
||||
}[] | Promise<{
|
||||
chave: string;
|
||||
rotulo: string;
|
||||
}[]>;
|
||||
limpavel?: boolean | undefined;
|
||||
erro?: boolean | undefined;
|
||||
mensagensErro?: string | string[] | undefined;
|
||||
dica?: string | undefined;
|
||||
dicaPersistente?: boolean | undefined;
|
||||
densidade?: import("../..").CampoDensidade | undefined;
|
||||
variante?: import("../..").CampoVariante | undefined;
|
||||
}];
|
||||
valor: unknown;
|
||||
}[]>;
|
||||
opcoesParaAdicionar: import("vue").ComputedRef<{
|
||||
chave: string;
|
||||
rotulo: string;
|
||||
filtro: (valor: unknown) => import("p-comuns").tipoFiltroDeltaColunaTipada<"small" | "length" | "concat" | "slice" | "indexOf" | "lastIndexOf" | "includes" | "toString" | "charAt" | "charCodeAt" | "localeCompare" | "match" | "replace" | "search" | "split" | "substring" | "toLowerCase" | "toLocaleLowerCase" | "toUpperCase" | "toLocaleUpperCase" | "trim" | "substr" | "codePointAt" | "endsWith" | "normalize" | "repeat" | "startsWith" | "anchor" | "big" | "blink" | "bold" | "fixed" | "fontcolor" | "fontsize" | "italics" | "link" | "strike" | "sub" | "sup" | "padStart" | "padEnd" | "trimEnd" | "trimStart" | "trimLeft" | "trimRight" | "matchAll" | "valueOf"> | undefined;
|
||||
entrada: ComponenteEntrada;
|
||||
}[]>;
|
||||
chaveParaAdicionar: Ref<string, string>;
|
||||
formulaCustomizada: Ref<string, string>;
|
||||
formulaGerada: import("vue").ComputedRef<string>;
|
||||
componenteEntrada: (entrada: ComponenteEntrada) => import("vue").DefineComponent<import("vue").ExtractPropTypes<{
|
||||
value: {
|
||||
type: import("vue").PropType<string | null | undefined>;
|
||||
default: undefined;
|
||||
};
|
||||
opcoes: {
|
||||
type: import("vue").PropType<{
|
||||
rotulo: string;
|
||||
placeholder?: string;
|
||||
} & {
|
||||
modo?: "data" | "dataHora";
|
||||
limpavel?: boolean;
|
||||
erro?: boolean;
|
||||
mensagensErro?: string | string[];
|
||||
dica?: string;
|
||||
dicaPersistente?: boolean;
|
||||
min?: string;
|
||||
max?: string;
|
||||
densidade?: import("../..").CampoDensidade;
|
||||
variante?: import("../..").CampoVariante;
|
||||
}>;
|
||||
required: false;
|
||||
default: undefined;
|
||||
};
|
||||
modelValue: {
|
||||
type: import("vue").PropType<string | null>;
|
||||
default: null;
|
||||
};
|
||||
modo: {
|
||||
type: import("vue").PropType<"data" | "dataHora" | undefined>;
|
||||
default: undefined;
|
||||
};
|
||||
rotulo: {
|
||||
type: StringConstructor;
|
||||
default: undefined;
|
||||
};
|
||||
placeholder: {
|
||||
type: StringConstructor;
|
||||
default: undefined;
|
||||
};
|
||||
desabilitado: {
|
||||
type: BooleanConstructor;
|
||||
default: undefined;
|
||||
};
|
||||
limpavel: {
|
||||
type: BooleanConstructor;
|
||||
default: undefined;
|
||||
};
|
||||
erro: {
|
||||
type: BooleanConstructor;
|
||||
default: undefined;
|
||||
};
|
||||
mensagensErro: {
|
||||
type: import("vue").PropType<string | string[]>;
|
||||
default: undefined;
|
||||
};
|
||||
dica: {
|
||||
type: StringConstructor;
|
||||
default: undefined;
|
||||
};
|
||||
dicaPersistente: {
|
||||
type: BooleanConstructor;
|
||||
default: undefined;
|
||||
};
|
||||
densidade: {
|
||||
type: import("vue").PropType<import("../..").CampoDensidade>;
|
||||
default: undefined;
|
||||
};
|
||||
variante: {
|
||||
type: import("vue").PropType<import("../..").CampoVariante>;
|
||||
default: undefined;
|
||||
};
|
||||
min: {
|
||||
type: import("vue").PropType<string | undefined>;
|
||||
default: undefined;
|
||||
};
|
||||
max: {
|
||||
type: import("vue").PropType<string | undefined>;
|
||||
default: undefined;
|
||||
};
|
||||
}>, {
|
||||
attrs: import("vue").Attrs;
|
||||
valor: import("vue").WritableComputedRef<string, string>;
|
||||
tipoInput: import("vue").ComputedRef<"date" | "datetime-local">;
|
||||
minLocal: import("vue").ComputedRef<string | undefined>;
|
||||
maxLocal: import("vue").ComputedRef<string | undefined>;
|
||||
opcoesEfetivas: import("vue").ComputedRef<{
|
||||
rotulo: string;
|
||||
placeholder?: string;
|
||||
} & {
|
||||
modo?: "data" | "dataHora";
|
||||
limpavel?: boolean;
|
||||
erro?: boolean;
|
||||
mensagensErro?: string | string[];
|
||||
dica?: string;
|
||||
dicaPersistente?: boolean;
|
||||
min?: string;
|
||||
max?: string;
|
||||
densidade?: import("../..").CampoDensidade;
|
||||
variante?: import("../..").CampoVariante;
|
||||
}>;
|
||||
desabilitadoEfetivo: import("vue").ComputedRef<boolean>;
|
||||
emitCompatFocus: () => void;
|
||||
emitCompatBlur: () => void;
|
||||
}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
|
||||
"update:value": (_v: string | null) => true;
|
||||
input: (_v: string | null) => true;
|
||||
change: (_v: string | null) => true;
|
||||
"update:modelValue": (_v: string | null) => true;
|
||||
alterar: (_v: string | null) => true;
|
||||
foco: () => true;
|
||||
desfoco: () => true;
|
||||
focus: () => true;
|
||||
blur: () => true;
|
||||
}, string, import("vue").PublicProps, Readonly<import("vue").ExtractPropTypes<{
|
||||
value: {
|
||||
type: import("vue").PropType<string | null | undefined>;
|
||||
default: undefined;
|
||||
};
|
||||
opcoes: {
|
||||
type: import("vue").PropType<{
|
||||
rotulo: string;
|
||||
placeholder?: string;
|
||||
} & {
|
||||
modo?: "data" | "dataHora";
|
||||
limpavel?: boolean;
|
||||
erro?: boolean;
|
||||
mensagensErro?: string | string[];
|
||||
dica?: string;
|
||||
dicaPersistente?: boolean;
|
||||
min?: string;
|
||||
max?: string;
|
||||
densidade?: import("../..").CampoDensidade;
|
||||
variante?: import("../..").CampoVariante;
|
||||
}>;
|
||||
required: false;
|
||||
default: undefined;
|
||||
};
|
||||
modelValue: {
|
||||
type: import("vue").PropType<string | null>;
|
||||
default: null;
|
||||
};
|
||||
modo: {
|
||||
type: import("vue").PropType<"data" | "dataHora" | undefined>;
|
||||
default: undefined;
|
||||
};
|
||||
rotulo: {
|
||||
type: StringConstructor;
|
||||
default: undefined;
|
||||
};
|
||||
placeholder: {
|
||||
type: StringConstructor;
|
||||
default: undefined;
|
||||
};
|
||||
desabilitado: {
|
||||
type: BooleanConstructor;
|
||||
default: undefined;
|
||||
};
|
||||
limpavel: {
|
||||
type: BooleanConstructor;
|
||||
default: undefined;
|
||||
};
|
||||
erro: {
|
||||
type: BooleanConstructor;
|
||||
default: undefined;
|
||||
};
|
||||
mensagensErro: {
|
||||
type: import("vue").PropType<string | string[]>;
|
||||
default: undefined;
|
||||
};
|
||||
dica: {
|
||||
type: StringConstructor;
|
||||
default: undefined;
|
||||
};
|
||||
dicaPersistente: {
|
||||
type: BooleanConstructor;
|
||||
default: undefined;
|
||||
};
|
||||
densidade: {
|
||||
type: import("vue").PropType<import("../..").CampoDensidade>;
|
||||
default: undefined;
|
||||
};
|
||||
variante: {
|
||||
type: import("vue").PropType<import("../..").CampoVariante>;
|
||||
default: undefined;
|
||||
};
|
||||
min: {
|
||||
type: import("vue").PropType<string | undefined>;
|
||||
default: undefined;
|
||||
};
|
||||
max: {
|
||||
type: import("vue").PropType<string | undefined>;
|
||||
default: undefined;
|
||||
};
|
||||
}>> & Readonly<{
|
||||
onInput?: ((_v: string | null) => any) | undefined;
|
||||
"onUpdate:value"?: ((_v: string | null) => any) | undefined;
|
||||
onChange?: ((_v: string | null) => any) | undefined;
|
||||
"onUpdate:modelValue"?: ((_v: string | null) => any) | undefined;
|
||||
onAlterar?: ((_v: string | null) => any) | undefined;
|
||||
onFoco?: (() => any) | undefined;
|
||||
onDesfoco?: (() => any) | undefined;
|
||||
onFocus?: (() => any) | undefined;
|
||||
onBlur?: (() => any) | undefined;
|
||||
}>, {
|
||||
modo: "data" | "dataHora" | undefined;
|
||||
limpavel: boolean;
|
||||
erro: boolean;
|
||||
mensagensErro: string | string[];
|
||||
dica: string;
|
||||
dicaPersistente: boolean;
|
||||
min: string | undefined;
|
||||
max: string | undefined;
|
||||
densidade: import("../..").CampoDensidade;
|
||||
variante: import("../..").CampoVariante;
|
||||
opcoes: {
|
||||
rotulo: string;
|
||||
placeholder?: string;
|
||||
} & {
|
||||
modo?: "data" | "dataHora";
|
||||
limpavel?: boolean;
|
||||
erro?: boolean;
|
||||
mensagensErro?: string | string[];
|
||||
dica?: string;
|
||||
dicaPersistente?: boolean;
|
||||
min?: string;
|
||||
max?: string;
|
||||
densidade?: import("../..").CampoDensidade;
|
||||
variante?: import("../..").CampoVariante;
|
||||
};
|
||||
value: string | null | undefined;
|
||||
modelValue: string | null;
|
||||
rotulo: string;
|
||||
placeholder: string;
|
||||
desabilitado: boolean;
|
||||
}, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any> | import("vue").DefineComponent<import("vue").ExtractPropTypes<{
|
||||
value: {
|
||||
type: import("vue").PropType<number | null | undefined>;
|
||||
default: undefined;
|
||||
};
|
||||
opcoes: {
|
||||
type: import("vue").PropType<{
|
||||
rotulo: string;
|
||||
placeholder?: string;
|
||||
} & {
|
||||
sufixo?: string;
|
||||
prefixo?: string;
|
||||
precisao?: number;
|
||||
}>;
|
||||
required: true;
|
||||
};
|
||||
}>, {
|
||||
attrs: import("vue").Attrs;
|
||||
emit: ((event: "input", _v: number | null | undefined) => void) & ((event: "update:value", _v: number | null | undefined) => void) & ((event: "change", _v: number | null | undefined) => void) & ((event: "focus") => void) & ((event: "blur") => void);
|
||||
displayValue: Ref<string, string>;
|
||||
isInteiro: import("vue").ComputedRef<boolean>;
|
||||
onUpdateModelValue: (texto: string) => void;
|
||||
}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
|
||||
"update:value": (_v: number | null | undefined) => true;
|
||||
input: (_v: number | null | undefined) => true;
|
||||
change: (_v: number | null | undefined) => true;
|
||||
focus: () => true;
|
||||
blur: () => true;
|
||||
}, string, import("vue").PublicProps, Readonly<import("vue").ExtractPropTypes<{
|
||||
value: {
|
||||
type: import("vue").PropType<number | null | undefined>;
|
||||
default: undefined;
|
||||
};
|
||||
opcoes: {
|
||||
type: import("vue").PropType<{
|
||||
rotulo: string;
|
||||
placeholder?: string;
|
||||
} & {
|
||||
sufixo?: string;
|
||||
prefixo?: string;
|
||||
precisao?: number;
|
||||
}>;
|
||||
required: true;
|
||||
};
|
||||
}>> & Readonly<{
|
||||
onInput?: ((_v: number | null | undefined) => any) | undefined;
|
||||
"onUpdate:value"?: ((_v: number | null | undefined) => any) | undefined;
|
||||
onChange?: ((_v: number | null | undefined) => any) | undefined;
|
||||
onFocus?: (() => any) | undefined;
|
||||
onBlur?: (() => any) | undefined;
|
||||
}>, {
|
||||
value: number | null | undefined;
|
||||
}, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any> | import("vue").DefineComponent<import("vue").ExtractPropTypes<{
|
||||
value: {
|
||||
type: import("vue").PropType<string | null | undefined>;
|
||||
default: undefined;
|
||||
};
|
||||
opcoes: {
|
||||
type: import("vue").PropType<{
|
||||
rotulo: string;
|
||||
placeholder?: string;
|
||||
} & {
|
||||
limiteCaracteres?: number;
|
||||
formato?: "texto" | "email" | "url" | "telefone" | "cpfCnpj" | "cep";
|
||||
}>;
|
||||
required: true;
|
||||
};
|
||||
}>, {
|
||||
attrs: import("vue").Attrs;
|
||||
emit: ((event: "input", _v: string | null | undefined) => void) & ((event: "update:value", _v: string | null | undefined) => void) & ((event: "change", _v: string | null | undefined) => void) & ((event: "focus") => void) & ((event: "blur") => void);
|
||||
localValue: import("vue").WritableComputedRef<string | null | undefined, string | null | undefined>;
|
||||
inputHtmlType: import("vue").ComputedRef<"text" | "email" | "url">;
|
||||
inputMode: import("vue").ComputedRef<string | undefined>;
|
||||
onInput: (e: Event) => void;
|
||||
}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
|
||||
"update:value": (_v: string | null | undefined) => true;
|
||||
input: (_v: string | null | undefined) => true;
|
||||
change: (_v: string | null | undefined) => true;
|
||||
focus: () => true;
|
||||
blur: () => true;
|
||||
}, string, import("vue").PublicProps, Readonly<import("vue").ExtractPropTypes<{
|
||||
value: {
|
||||
type: import("vue").PropType<string | null | undefined>;
|
||||
default: undefined;
|
||||
};
|
||||
opcoes: {
|
||||
type: import("vue").PropType<{
|
||||
rotulo: string;
|
||||
placeholder?: string;
|
||||
} & {
|
||||
limiteCaracteres?: number;
|
||||
formato?: "texto" | "email" | "url" | "telefone" | "cpfCnpj" | "cep";
|
||||
}>;
|
||||
required: true;
|
||||
};
|
||||
}>> & Readonly<{
|
||||
onInput?: ((_v: string | null | undefined) => any) | undefined;
|
||||
"onUpdate:value"?: ((_v: string | null | undefined) => any) | undefined;
|
||||
onChange?: ((_v: string | null | undefined) => any) | undefined;
|
||||
onFocus?: (() => any) | undefined;
|
||||
onBlur?: (() => any) | undefined;
|
||||
}>, {
|
||||
value: string | null | undefined;
|
||||
}, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
|
||||
opcoesEntrada: (entrada: ComponenteEntrada) => {
|
||||
rotulo: string;
|
||||
};
|
||||
adicionar: () => void;
|
||||
remover: (idx: number) => void;
|
||||
emitFechar: () => void;
|
||||
emitSalvar: () => void;
|
||||
emitLimpar: () => void;
|
||||
rotuloDoFiltro: typeof rotuloDoFiltro;
|
||||
rotuloDoFiltroAtivo: (chave: string) => string;
|
||||
};
|
||||
export {};
|
||||
3
dist/types/index.d.ts
vendored
3
dist/types/index.d.ts
vendored
|
|
@ -1,10 +1,11 @@
|
|||
import type { Plugin } from "vue";
|
||||
import "./styles/eli-vue-fonts.css";
|
||||
import "./design-tokens.css";
|
||||
import { EliBotao } from "./componentes/botao";
|
||||
import { EliCartao } from "./componentes/cartao";
|
||||
import { EliBadge } from "./componentes/indicador";
|
||||
import { EliOlaMundo } from "./componentes/ola_mundo";
|
||||
export { criarFiltro26 } from "p-comuns";
|
||||
export { iniciarFiltroDelta } from "p-comuns";
|
||||
export * from "./componentes/EliEntrada";
|
||||
export * from "./componentes/EliTabela";
|
||||
export * from "./tipos";
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "eli-vue",
|
||||
"version": "0.1.144",
|
||||
"version": "0.1.183",
|
||||
"private": false,
|
||||
"main": "./dist/eli-vue.umd.js",
|
||||
"module": "./dist/eli-vue.es.js",
|
||||
|
|
@ -15,7 +15,7 @@
|
|||
"scripts": {
|
||||
"biome": "pnpm exec biome check --write",
|
||||
"check": "pnpm run biome && npx tsc --noEmit",
|
||||
"prebuild": "npm version patch --no-git-tag-version",
|
||||
"prebuild": "npm version patch --no-git-tag-version && bash scripts/check-linhas.sh",
|
||||
"build:types": "vue-tsc -p tsconfig.build.json --declaration --emitDeclarationOnly",
|
||||
"build": "pnpm run biome && vite build && npm run build:types",
|
||||
"dev": "pnpm run biome && pnpm run fechar && vite",
|
||||
|
|
@ -41,6 +41,7 @@
|
|||
},
|
||||
"dependencies": {
|
||||
"@lucide/vue": "^1.16.0",
|
||||
"alasql": "^4.17.3",
|
||||
"cross-fetch": "^4.1.0",
|
||||
"dayjs": "^1.11.19",
|
||||
"p-comuns": "git+https://git2.idz.one/publico/_comuns.git",
|
||||
|
|
|
|||
2728
pnpm-lock.yaml
generated
2728
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load diff
49
scripts/check-linhas.sh
Executable file
49
scripts/check-linhas.sh
Executable file
|
|
@ -0,0 +1,49 @@
|
|||
#!/usr/bin/env bash
|
||||
# check-linhas.sh
|
||||
# Garante que nenhum arquivo *.vue, *.ts ou *.tsx ultrapasse o limite de linhas definido.
|
||||
# Executado automaticamente no prebuild via package.json.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
LIMITE=250
|
||||
|
||||
# Arquivos isentos permanentes (gerados, dados de teste, etc.)
|
||||
# Formato: caminho relativo a partir da raiz do projeto
|
||||
ISENTOS=(
|
||||
|
||||
)
|
||||
|
||||
RAIZ="$(cd "$(dirname "$0")/.." && pwd)"
|
||||
ERROS=0
|
||||
|
||||
while IFS= read -r arquivo; do
|
||||
relativo="${arquivo#$RAIZ/}"
|
||||
|
||||
# Verifica se está na lista de isentos
|
||||
isento=false
|
||||
for i in "${ISENTOS[@]}"; do
|
||||
if [ "$relativo" = "$i" ]; then
|
||||
isento=true
|
||||
break
|
||||
fi
|
||||
done
|
||||
|
||||
if $isento; then
|
||||
continue
|
||||
fi
|
||||
|
||||
linhas=$(wc -l < "$arquivo")
|
||||
if [ "$linhas" -gt "$LIMITE" ]; then
|
||||
echo "❌ $relativo ($linhas linhas) — limite: $LIMITE"
|
||||
ERROS=$((ERROS + 1))
|
||||
fi
|
||||
done < <(find "$RAIZ/src" \( -name "*.vue" -o -name "*.ts" -o -name "*.tsx" \) -not -path "*/node_modules/*")
|
||||
|
||||
if [ "$ERROS" -gt 0 ]; then
|
||||
echo ""
|
||||
echo "⚠️ $ERROS arquivo(s) ultrapassam o limite de $LIMITE linhas."
|
||||
echo " Estratégias: subcomponentes, composables use*.ts, ou separar CSS."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "✅ Todos os arquivos (*.vue, *.ts, *.tsx) estão dentro do limite de $LIMITE linhas."
|
||||
59
scripts/check-vue-linhas.sh
Executable file
59
scripts/check-vue-linhas.sh
Executable file
|
|
@ -0,0 +1,59 @@
|
|||
#!/usr/bin/env bash
|
||||
# check-vue-linhas.sh
|
||||
# Garante que nenhum arquivo *.vue ultrapasse o limite de linhas definido.
|
||||
#
|
||||
# Arquivos listados em ISENTOS são legados em processo de refatoração —
|
||||
# cada um deve ser quebrado em componentes menores antes de ser removido daqui.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
LIMITE=250
|
||||
|
||||
# Todos os arquivos *.vue devem respeitar o limite.
|
||||
# Não existem isentos.
|
||||
ISENTOS=()
|
||||
|
||||
RAIZ="$(cd "$(dirname "$0")/.." && pwd)"
|
||||
ERROS=0
|
||||
|
||||
# Constrói padrão grep para os isentos
|
||||
isentos_grep=""
|
||||
for isento in "${ISENTOS[@]}"; do
|
||||
if [ -n "$isentos_grep" ]; then
|
||||
isentos_grep="${isentos_grep}\|${isento}"
|
||||
else
|
||||
isentos_grep="${isento}"
|
||||
fi
|
||||
done
|
||||
|
||||
while IFS= read -r arquivo; do
|
||||
relativo="${arquivo#$RAIZ/}"
|
||||
|
||||
# Verifica se está na lista de isentos
|
||||
isento=false
|
||||
for i in "${ISENTOS[@]}"; do
|
||||
if [ "$relativo" = "$i" ]; then
|
||||
isento=true
|
||||
break
|
||||
fi
|
||||
done
|
||||
|
||||
if $isento; then
|
||||
continue
|
||||
fi
|
||||
|
||||
linhas=$(wc -l < "$arquivo")
|
||||
if [ "$linhas" -gt "$LIMITE" ]; then
|
||||
echo "❌ $relativo ($linhas linhas) — limite: $LIMITE"
|
||||
ERROS=$((ERROS + 1))
|
||||
fi
|
||||
done < <(find "$RAIZ/src" -name "*.vue" -not -path "*/node_modules/*")
|
||||
|
||||
if [ "$ERROS" -gt 0 ]; then
|
||||
echo ""
|
||||
echo "⚠️ $ERROS arquivo(s) Vue ultrapassam o limite de $LIMITE linhas."
|
||||
echo " Quebre em componentes menores antes de fazer build."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "✅ Todos os arquivos Vue estão dentro do limite de $LIMITE linhas."
|
||||
|
|
@ -23,7 +23,7 @@
|
|||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { dayjsbr } from "p-comuns"
|
||||
import dayjs from "dayjs"
|
||||
import { computed, defineComponent, type PropType } from "vue"
|
||||
import type { CampoDensidade, CampoVariante } from "../../tipos"
|
||||
import type { PadroesEntradas } from "./tiposEntradas"
|
||||
|
|
@ -122,13 +122,9 @@ export default defineComponent({
|
|||
}
|
||||
})
|
||||
|
||||
const modoEfetivo = computed<"data" | "dataHora">(
|
||||
() => opcoesEfetivas.value.modo ?? "dataHora",
|
||||
)
|
||||
const modoEfetivo = computed<"data" | "dataHora">(() => opcoesEfetivas.value.modo ?? "dataHora")
|
||||
|
||||
const desabilitadoEfetivo = computed<boolean>(() =>
|
||||
Boolean(props.desabilitado),
|
||||
)
|
||||
const desabilitadoEfetivo = computed<boolean>(() => Boolean(props.desabilitado))
|
||||
|
||||
const tipoInput = computed<"date" | "datetime-local">(() =>
|
||||
modoEfetivo.value === "data" ? "date" : "datetime-local",
|
||||
|
|
@ -136,23 +132,21 @@ export default defineComponent({
|
|||
|
||||
function isoParaInputDatetime(valorIso: string): string {
|
||||
if (modoEfetivo.value === "data") {
|
||||
return dayjsbr(valorIso).format("YYYY-MM-DD")
|
||||
return dayjs(valorIso).format("YYYY-MM-DD")
|
||||
}
|
||||
return dayjsbr(valorIso).format("YYYY-MM-DDTHH:mm")
|
||||
return dayjs(valorIso).format("YYYY-MM-DDTHH:mm")
|
||||
}
|
||||
|
||||
function inputDatetimeParaIsoLocal(valorInput: string): string {
|
||||
if (modoEfetivo.value === "data") {
|
||||
return dayjsbr(`${valorInput}T00:00`).format()
|
||||
return dayjs(`${valorInput}T00:00`).format()
|
||||
}
|
||||
return dayjsbr(valorInput).format()
|
||||
return dayjs(valorInput).format()
|
||||
}
|
||||
|
||||
const effectiveModelValue = computed<string | null>(() => {
|
||||
// Prioridade: value (novo) se vier definido; senão usa modelValue (antigo)
|
||||
return props.value !== undefined
|
||||
? (props.value ?? null)
|
||||
: props.modelValue
|
||||
return props.value !== undefined ? (props.value ?? null) : props.modelValue
|
||||
})
|
||||
|
||||
const valor = computed<string>({
|
||||
|
|
|
|||
14
src/componentes/EliEntrada/EliEntradaNumero.css
Normal file
14
src/componentes/EliEntrada/EliEntradaNumero.css
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
.eli-entrada__prefixo,
|
||||
.eli-entrada__sufixo {
|
||||
opacity: 0.75;
|
||||
font-size: 0.9em;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.eli-entrada__prefixo {
|
||||
margin-right: 6px;
|
||||
}
|
||||
|
||||
.eli-entrada__sufixo {
|
||||
margin-left: 6px;
|
||||
}
|
||||
112
src/componentes/EliEntrada/EliEntradaNumero.helpers.ts
Normal file
112
src/componentes/EliEntrada/EliEntradaNumero.helpers.ts
Normal file
|
|
@ -0,0 +1,112 @@
|
|||
/**
|
||||
* Helpers de formatação e parsing numérico usados pelo EliEntradaNumero.
|
||||
* Extraídos do componente Vue para manter o arquivo dentro de 250 linhas.
|
||||
*/
|
||||
|
||||
export function casasDecimaisFromPrecisao(precisao: number): number {
|
||||
if (!Number.isFinite(precisao) || precisao <= 0) return 0
|
||||
if (precisao >= 1) return 0
|
||||
const texto = precisao.toString()
|
||||
if (texto.includes("e-")) {
|
||||
const [, exp] = texto.split("e-")
|
||||
const n = Number(exp)
|
||||
return Number.isFinite(n) ? n : 0
|
||||
}
|
||||
const idx = texto.indexOf(".")
|
||||
if (idx === -1) return 0
|
||||
const dec = texto.slice(idx + 1).replace(/0+$/, "")
|
||||
return dec.length
|
||||
}
|
||||
|
||||
/**
|
||||
* Analisa a entrada de texto e converte em um valor numérico válido (float).
|
||||
* Troca a vírgula brasileira por ponto antes de passar para o parser nativo.
|
||||
*
|
||||
* @param texto - O texto bruto digitado ou vindo do estado
|
||||
* @returns O número processado ou `null` caso inválido ou vazio
|
||||
*/
|
||||
export function parseNumero(texto: string): number | null {
|
||||
const normalizado = (texto ?? "").trim().replace(/,/g, ".")
|
||||
if (!normalizado) return null
|
||||
const n = Number(normalizado)
|
||||
if (Number.isNaN(n)) return null
|
||||
return n
|
||||
}
|
||||
|
||||
/**
|
||||
* Formata um valor numérico para a exibição na interface (usando vírgula como separador).
|
||||
*
|
||||
* @param value - O valor numérico de fato emitido
|
||||
* @param casas - Opcionalmente, o número exato de casas decimais para fixar
|
||||
* @returns String formatada amigável para exibição em pt-BR
|
||||
*/
|
||||
export function formatarNumero(value: number | null | undefined, casas: number | null) {
|
||||
if (value === null || value === undefined) return ""
|
||||
if (casas === null) return String(value).replace(/\./g, ",")
|
||||
const fixed = Number(value).toFixed(Math.max(0, casas))
|
||||
return fixed.replace(/\./g, ",")
|
||||
}
|
||||
|
||||
/**
|
||||
* Retira todo tipo de sujeira de uma string, devolvendo restritamente números (para inteiros).
|
||||
*
|
||||
* @param texto - String de entrada
|
||||
* @returns Apenas dígitos de 0 a 9
|
||||
*/
|
||||
export function somenteNumeros(texto: string) {
|
||||
return (texto ?? "").replace(/\D+/g, "")
|
||||
}
|
||||
|
||||
/**
|
||||
* Higieniza o texto preservando apenas os dígitos e exatamente UM separador decimal.
|
||||
*
|
||||
* @param texto - Texto misto de entrada
|
||||
* @returns String contendo número e (opcionalmente) uma vírgula ou ponto residual
|
||||
*/
|
||||
export function somenteNumerosESeparadorDecimal(texto: string) {
|
||||
const limpo = (texto ?? "").replace(/[^0-9.,]+/g, "")
|
||||
const matchSep = limpo.match(/[.,]/)
|
||||
if (!matchSep) return limpo
|
||||
|
||||
const sep = matchSep[0]
|
||||
const idx = limpo.indexOf(sep)
|
||||
const antesRaw = limpo.slice(0, idx).replace(/[.,]/g, "")
|
||||
const depois = limpo.slice(idx + 1).replace(/[.,]/g, "")
|
||||
const antes = antesRaw.length ? antesRaw : "0"
|
||||
return `${antes}${sep}${depois}`
|
||||
}
|
||||
|
||||
/**
|
||||
* Trunca a parte decimal de um texto numérico ao número máximo de casas permitidas.
|
||||
* Aplicado durante a digitação para evitar que o usuário ultrapasse a precisão configurada.
|
||||
*
|
||||
* @param texto - Texto numérico já higienizado (com separador único)
|
||||
* @param casas - Número máximo de casas decimais; `null` = sem limite
|
||||
* @returns Texto com a parte decimal truncada no limite especificado
|
||||
*/
|
||||
export function limitarCasasDecimais(texto: string, casas: number | null) {
|
||||
if (casas === null) return texto
|
||||
if (casas <= 0) return texto.replace(/[.,]/g, "")
|
||||
|
||||
const matchSep = texto.match(/[.,]/)
|
||||
if (!matchSep) return texto
|
||||
const sep = matchSep[0]
|
||||
const idx = texto.indexOf(sep)
|
||||
|
||||
const inteiro = texto.slice(0, idx)
|
||||
const frac = texto.slice(idx + 1)
|
||||
return `${inteiro}${sep}${frac.slice(0, casas)}`
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve o edge-case de digitação transitória parcial terminada em separador (ex: "1,").
|
||||
*
|
||||
* @param texto - O texto já higienizado, tipicamente terminando em vírgula ou ponto
|
||||
* @returns A conversão parcial estrita ou `null` caso falhe
|
||||
*/
|
||||
export function valorParcialDecimal(texto: string): number | null {
|
||||
const m = texto.match(/^(\d+)[.,]$/)
|
||||
if (!m) return null
|
||||
const n = Number(m[1])
|
||||
return Number.isNaN(n) ? null : n
|
||||
}
|
||||
|
|
@ -27,126 +27,19 @@
|
|||
<script lang="ts">
|
||||
import { computed, defineComponent, type PropType, ref, watch } from "vue"
|
||||
import type { PadroesEntradas } from "./tiposEntradas"
|
||||
import "./EliEntradaNumero.css"
|
||||
import {
|
||||
casasDecimaisFromPrecisao,
|
||||
formatarNumero,
|
||||
limitarCasasDecimais,
|
||||
parseNumero,
|
||||
somenteNumeros,
|
||||
somenteNumerosESeparadorDecimal,
|
||||
valorParcialDecimal,
|
||||
} from "./EliEntradaNumero.helpers"
|
||||
|
||||
type EntradaNumero = PadroesEntradas["numero"]
|
||||
|
||||
function casasDecimaisFromPrecisao(precisao: number): number {
|
||||
if (!Number.isFinite(precisao) || precisao <= 0) return 0
|
||||
if (precisao >= 1) return 0
|
||||
|
||||
// Preferência por contar casas decimais na representação (ex.: 0.01 -> 2)
|
||||
const texto = precisao.toString()
|
||||
if (texto.includes("e-")) {
|
||||
const [, exp] = texto.split("e-")
|
||||
const n = Number(exp)
|
||||
return Number.isFinite(n) ? n : 0
|
||||
}
|
||||
const idx = texto.indexOf(".")
|
||||
if (idx === -1) return 0
|
||||
const dec = texto.slice(idx + 1).replace(/0+$/, "")
|
||||
return dec.length
|
||||
}
|
||||
|
||||
/**
|
||||
* Analisa a entrada de texto e converte em um valor numérico válido (float).
|
||||
* Troca a vírgula brasileira por ponto antes de passar para o parser nativo.
|
||||
*
|
||||
* @param texto - O texto bruto digitado ou vindo do estado
|
||||
* @returns O número processado ou `null` caso inválido ou vazio
|
||||
*/
|
||||
function parseNumero(texto: string): number | null {
|
||||
const normalizado = (texto ?? "").trim().replace(/,/g, ".")
|
||||
if (!normalizado) return null
|
||||
const n = Number(normalizado)
|
||||
if (Number.isNaN(n)) return null
|
||||
return n
|
||||
}
|
||||
|
||||
/**
|
||||
* Formata um valor numérico para a exibição na interface (usando vírgula como separador).
|
||||
* Em caso de precisão nula (modo livre), preserva a semântica de exibição direta sem forçar o preenchimento de zeros (toFixed).
|
||||
*
|
||||
* @param value - O valor numérico de fato emitido
|
||||
* @param casas - Opcionalmente, o número exato de casas decimais para fixar
|
||||
* @returns String formatada amigável para exibição em pt-BR
|
||||
*/
|
||||
function formatarNumero(
|
||||
value: number | null | undefined,
|
||||
casas: number | null,
|
||||
) {
|
||||
if (value === null || value === undefined) return ""
|
||||
if (casas === null) return String(value).replace(/\./g, ",")
|
||||
|
||||
// Garantimos que o texto visual corresponda ao value (fixed).
|
||||
const fixed = Number(value).toFixed(Math.max(0, casas))
|
||||
// Exibe com vírgula (pt-BR)
|
||||
return fixed.replace(/\./g, ",")
|
||||
}
|
||||
|
||||
/**
|
||||
* Retira todo tipo de sujeira de uma string, devolvendo restritamente números (para inteiros).
|
||||
*
|
||||
* @param texto - String de entrada
|
||||
* @returns Apenas dígitos de 0 a 9
|
||||
*/
|
||||
function somenteNumeros(texto: string) {
|
||||
return (texto ?? "").replace(/\D+/g, "")
|
||||
}
|
||||
|
||||
/**
|
||||
* Higieniza o texto preservando apenas os dígitos e exatamente UM separador decimal.
|
||||
* Colapsa múltiplos separadores e normaliza se o separador vier no início (",4" => "0,4").
|
||||
*
|
||||
* @param texto - Texto misto de entrada
|
||||
* @returns String contendo número e (opcionalmente) uma vírgula ou ponto residual
|
||||
*/
|
||||
function somenteNumerosESeparadorDecimal(texto: string) {
|
||||
// Mantém apenas números e UM separador decimal ("," ou "."), sem sinais.
|
||||
// Implementação baseada em regex + colapso de separadores.
|
||||
const limpo = (texto ?? "").replace(/[^0-9.,]+/g, "")
|
||||
const matchSep = limpo.match(/[.,]/)
|
||||
if (!matchSep) return limpo
|
||||
|
||||
const sep = matchSep[0]
|
||||
const idx = limpo.indexOf(sep)
|
||||
const antesRaw = limpo.slice(0, idx).replace(/[.,]/g, "")
|
||||
const depois = limpo.slice(idx + 1).replace(/[.,]/g, "")
|
||||
|
||||
// Se o usuário começa pelo separador (",4"), normalizamos para "0,4".
|
||||
const antes = antesRaw.length ? antesRaw : "0"
|
||||
return `${antes}${sep}${depois}`
|
||||
}
|
||||
|
||||
function limitarCasasDecimais(texto: string, casas: number | null) {
|
||||
if (casas === null) return texto
|
||||
if (casas <= 0) return texto.replace(/[.,]/g, "")
|
||||
|
||||
const matchSep = texto.match(/[.,]/)
|
||||
if (!matchSep) return texto
|
||||
const sep = matchSep[0]
|
||||
const idx = texto.indexOf(sep)
|
||||
|
||||
const inteiro = texto.slice(0, idx)
|
||||
const frac = texto.slice(idx + 1)
|
||||
return `${inteiro}${sep}${frac.slice(0, casas)}`
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve o edge-case de digitação transitória parcial terminada em separador (ex: "1,").
|
||||
* Identifica se a string tem este formato pendente e retorna temporariamente sua base inteira
|
||||
* sem permitir que o valor se torne nulo.
|
||||
*
|
||||
* @param texto - O texto já higienizado, tipicamente terminando em vírgula ou ponto
|
||||
* @returns A conversão parcial estrita ou `null` caso falhe
|
||||
*/
|
||||
function valorParcialDecimal(texto: string): number | null {
|
||||
// Regra (B): se o usuário digitou "1," ou "1." emite 1.
|
||||
const m = texto.match(/^(\d+)[.,]$/)
|
||||
if (!m) return null
|
||||
const n = Number(m[1])
|
||||
return Number.isNaN(n) ? null : n
|
||||
}
|
||||
|
||||
export default defineComponent({
|
||||
name: "EliEntradaNumero",
|
||||
inheritAttrs: false,
|
||||
|
|
@ -229,12 +122,8 @@ export default defineComponent({
|
|||
}
|
||||
|
||||
// Modo livre (sem precisao) ou inteiro: aceita números e 1 separador (no caso livre)
|
||||
const base = isInteiro.value
|
||||
? somenteNumeros(texto)
|
||||
: somenteNumerosESeparadorDecimal(texto)
|
||||
const textoFiltrado = isInteiro.value
|
||||
? base
|
||||
: limitarCasasDecimais(base, casasDecimais.value)
|
||||
const base = isInteiro.value ? somenteNumeros(texto) : somenteNumerosESeparadorDecimal(texto)
|
||||
const textoFiltrado = isInteiro.value ? base : limitarCasasDecimais(base, casasDecimais.value)
|
||||
|
||||
// Emissão:
|
||||
// - vazio => null
|
||||
|
|
@ -242,9 +131,7 @@ export default defineComponent({
|
|||
// - caso geral => parse normal
|
||||
let out: EntradaNumero["value"] = null
|
||||
if (textoFiltrado) {
|
||||
const parcial = isInteiro.value
|
||||
? null
|
||||
: valorParcialDecimal(textoFiltrado)
|
||||
const parcial = isInteiro.value ? null : valorParcialDecimal(textoFiltrado)
|
||||
const n = parcial ?? parseNumero(textoFiltrado)
|
||||
out = (n === null ? null : n) as EntradaNumero["value"]
|
||||
}
|
||||
|
|
@ -269,19 +156,3 @@ export default defineComponent({
|
|||
})
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.eli-entrada__prefixo,
|
||||
.eli-entrada__sufixo {
|
||||
opacity: 0.75;
|
||||
font-size: 0.9em;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.eli-entrada__prefixo {
|
||||
margin-right: 6px;
|
||||
}
|
||||
|
||||
.eli-entrada__sufixo {
|
||||
margin-left: 6px;
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -23,14 +23,7 @@
|
|||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import {
|
||||
computed,
|
||||
defineComponent,
|
||||
onMounted,
|
||||
type PropType,
|
||||
ref,
|
||||
watch,
|
||||
} from "vue"
|
||||
import { computed, defineComponent, onMounted, type PropType, ref, watch } from "vue"
|
||||
import { VSelect } from "vuetify/components"
|
||||
import type { PadroesEntradas } from "./tiposEntradas"
|
||||
|
||||
|
|
|
|||
|
|
@ -65,8 +65,7 @@ export default defineComponent({
|
|||
|
||||
const inputMode = computed<string | undefined>(() => {
|
||||
if (formato.value === "telefone") return "tel"
|
||||
if (formato.value === "cpfCnpj" || formato.value === "cep")
|
||||
return "numeric"
|
||||
if (formato.value === "cpfCnpj" || formato.value === "cep") return "numeric"
|
||||
return undefined
|
||||
})
|
||||
|
||||
|
|
|
|||
|
|
@ -7,6 +7,16 @@ import EliEntradaTexto from "./EliEntradaTexto.vue"
|
|||
|
||||
import type { TipoEntrada } from "./tiposEntradas"
|
||||
|
||||
/**
|
||||
* Mapa (dispatch table) que associa cada `TipoEntrada` ao seu componente Vue correspondente.
|
||||
* Usado pelo sistema de filtro avançado para renderizar dinamicamente o campo correto
|
||||
* a partir da configuração declarada em `tipoEliTabelaConsulta.filtroAvancado[].entrada`.
|
||||
*
|
||||
* Adicionar um novo tipo de entrada requer:
|
||||
* 1. Criar o componente Vue
|
||||
* 2. Adicionar a chave em `TipoEntrada` (tiposEntradas.ts)
|
||||
* 3. Registrar aqui no mapa
|
||||
*/
|
||||
export const registryTabelaCelulas = {
|
||||
texto: EliEntradaTexto,
|
||||
numero: EliEntradaNumero,
|
||||
|
|
|
|||
|
|
@ -2,6 +2,14 @@ function somenteNumeros(v: string): string {
|
|||
return v.replace(/\D+/g, "")
|
||||
}
|
||||
|
||||
/**
|
||||
* Formata um CPF ou CNPJ aplicando a máscara brasileira padrão durante a digitação.
|
||||
* Detecta automaticamente o tipo pelo comprimento (≤11 dígitos = CPF, >11 = CNPJ).
|
||||
* Não valida o dígito verificador — apenas aplica a formatação visual.
|
||||
*
|
||||
* @param v - Valor bruto (pode conter letras, pontos, traços etc.)
|
||||
* @returns String formatada no padrão `000.000.000-00` (CPF) ou `00.000.000/0000-00` (CNPJ)
|
||||
*/
|
||||
export function formatarCpfCnpj(v: string): string {
|
||||
const d = somenteNumeros(v)
|
||||
|
||||
|
|
|
|||
|
|
@ -2,6 +2,20 @@
|
|||
width: 100%;
|
||||
}
|
||||
|
||||
.eli-tabela__table-wrapper {
|
||||
width: 100%;
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.eli-tabela__td--estados {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.eli-tabela__estados-inline {
|
||||
border: none;
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
.eli-tabela__table {
|
||||
width: 100%;
|
||||
border-collapse: separate;
|
||||
|
|
@ -29,21 +43,30 @@
|
|||
overflow: visible;
|
||||
}
|
||||
|
||||
.eli-tabela__tbody .eli-tabela__tr .eli-tabela__td {
|
||||
background: var(--cor-eli-superficie, #ffffff);
|
||||
}
|
||||
|
||||
.eli-tabela__tbody .eli-tabela__tr--zebra .eli-tabela__td {
|
||||
background: rgba(15, 23, 42, 0.02);
|
||||
background: var(--cor-eli-primaria-zebra, #F6F9E8);
|
||||
}
|
||||
|
||||
.eli-tabela__th,
|
||||
.eli-tabela__td {
|
||||
padding: 10px 12px;
|
||||
border-bottom: 1px solid rgba(0, 0, 0, 0.08);
|
||||
border-bottom: 1px solid var(--cor-eli-borda);
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
.eli-tabela__th {
|
||||
text-align: left;
|
||||
font-weight: 600;
|
||||
background: rgba(0, 0, 0, 0.03);
|
||||
font-size: 0.82rem;
|
||||
letter-spacing: 0.02em;
|
||||
text-transform: uppercase;
|
||||
color: var(--cor-eli-neutro-escuro, #3E4436);
|
||||
background: var(--cor-eli-primaria-suave, #BFC99C);
|
||||
border-bottom: 2px solid var(--cor-eli-primaria-bg, #EEF3D0);
|
||||
}
|
||||
|
||||
.eli-tabela__th--ordenavel {
|
||||
|
|
@ -71,16 +94,16 @@
|
|||
|
||||
.eli-tabela__th-botao:hover,
|
||||
.eli-tabela__th-botao:focus-visible {
|
||||
color: rgba(15, 23, 42, 0.85);
|
||||
color: var(--cor-eli-primaria-texto, #2E3F00);
|
||||
}
|
||||
|
||||
.eli-tabela__th-botao:focus-visible {
|
||||
outline: 2px solid rgba(37, 99, 235, 0.45);
|
||||
outline: 2px solid var(--cor-eli-primaria, #688C00);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
.eli-tabela__th-botao--ativo {
|
||||
color: rgba(15, 23, 42, 0.85);
|
||||
color: var(--cor-eli-primaria-texto, #2E3F00);
|
||||
}
|
||||
|
||||
.eli-tabela__th-texto {
|
||||
|
|
@ -110,14 +133,19 @@
|
|||
}
|
||||
|
||||
|
||||
|
||||
.eli-tabela--erro {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 10px;
|
||||
border: 1px solid rgba(220, 53, 69, 0.35);
|
||||
border-radius: 12px;
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
.eli-tabela--carregando {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
border: 1px dashed rgba(0, 0, 0, 0.25);
|
||||
border-radius: 12px;
|
||||
padding: 12px;
|
||||
|
|
@ -134,12 +162,33 @@
|
|||
}
|
||||
|
||||
.eli-tabela--vazio {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
border: 1px dashed rgba(0, 0, 0, 0.25);
|
||||
border-radius: 12px;
|
||||
padding: 12px;
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.eli-tabela__estado-icone {
|
||||
flex-shrink: 0;
|
||||
color: rgba(15, 23, 42, 0.45);
|
||||
}
|
||||
|
||||
.eli-tabela__estado-icone--erro {
|
||||
color: var(--cor-eli-erro);
|
||||
}
|
||||
|
||||
.eli-tabela__estado-icone--spin {
|
||||
animation: eli-tabela-spin 1s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes eli-tabela-spin {
|
||||
from { transform: rotate(0deg); }
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
.eli-tabela__th--acoes {
|
||||
text-align: right;
|
||||
white-space: nowrap;
|
||||
|
|
@ -222,8 +271,8 @@
|
|||
padding: 0 14px;
|
||||
border-radius: var(--eli-tabela-cabecalho-controle-radius);
|
||||
border: none;
|
||||
background: rgba(37, 99, 235, 0.12);
|
||||
color: rgba(37, 99, 235, 0.95);
|
||||
background: var(--cor-eli-primaria-bg);
|
||||
color: var(--cor-eli-primaria-escura);
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
line-height: 1;
|
||||
|
|
@ -233,11 +282,11 @@
|
|||
|
||||
.eli-tabela__acoes-cabecalho-botao:hover,
|
||||
.eli-tabela__acoes-cabecalho-botao:focus-visible {
|
||||
background: rgba(37, 99, 235, 0.2);
|
||||
background: var(--cor-eli-primaria-clara);
|
||||
}
|
||||
|
||||
.eli-tabela__acoes-cabecalho-botao:focus-visible {
|
||||
outline: 2px solid rgba(37, 99, 235, 0.35);
|
||||
outline: 2px solid var(--cor-eli-primaria-clara);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
|
|
@ -274,7 +323,7 @@
|
|||
}
|
||||
|
||||
.eli-tabela__acoes-toggle:focus-visible {
|
||||
outline: 2px solid rgba(37, 99, 235, 0.45);
|
||||
outline: 2px solid var(--cor-eli-primaria-clara);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
|
|
@ -289,7 +338,7 @@
|
|||
padding: 6px 0;
|
||||
margin: 0;
|
||||
list-style: none;
|
||||
background: #ffffff;
|
||||
background: var(--cor-eli-superficie);
|
||||
border: 1px solid rgba(15, 23, 42, 0.08);
|
||||
border-radius: 10px;
|
||||
box-shadow: 0 12px 30px rgba(15, 23, 42, 0.18);
|
||||
|
|
@ -365,7 +414,7 @@
|
|||
}
|
||||
|
||||
.eli-tabela__expander-botao:focus-visible {
|
||||
outline: 2px solid rgba(37, 99, 235, 0.45);
|
||||
outline: 2px solid var(--cor-eli-primaria-clara);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
|
|
@ -445,8 +494,8 @@
|
|||
|
||||
.eli-tabela__checkbox:checked,
|
||||
.eli-tabela__checkbox:indeterminate {
|
||||
background-color: #1976d2;
|
||||
border-color: #1976d2;
|
||||
background-color: var(--cor-eli-primaria);
|
||||
border-color: var(--cor-eli-primaria);
|
||||
}
|
||||
|
||||
.eli-tabela__checkbox:checked::after {
|
||||
|
|
@ -473,6 +522,6 @@
|
|||
}
|
||||
|
||||
.eli-tabela__checkbox:focus-visible {
|
||||
box-shadow: 0 0 0 2px rgba(25, 118, 210, 0.3);
|
||||
box-shadow: 0 0 0 2px var(--cor-eli-primaria-clara);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -31,57 +31,60 @@
|
|||
|
||||
<EliTabelaModalFiltroAvancado
|
||||
:aberto="modalFiltroAberto"
|
||||
:filtrosBase="tabela.filtroAvancado ?? []"
|
||||
:filtrosBase="(tabela.filtroAvancado ?? []) as FiltroBaseModal[]"
|
||||
:modelo="filtrosUi"
|
||||
:modeloFormula="filtrosFormula"
|
||||
@fechar="fecharModalFiltro"
|
||||
@limpar="limparFiltrosAvancados"
|
||||
@salvar="salvarFiltrosAvancados"
|
||||
/>
|
||||
|
||||
<table class="eli-tabela__table">
|
||||
<EliTabelaHead
|
||||
:colunas="colunasEfetivas"
|
||||
:temAcoes="temAcoes"
|
||||
:temColunasInvisiveis="temColunasInvisiveis"
|
||||
:colunaOrdenacao="colunaOrdenacao"
|
||||
:direcaoOrdenacao="direcaoOrdenacao"
|
||||
:mostrarSelecao="mostrarSelecao"
|
||||
:todosSelecionados="todosSelecionados"
|
||||
:algunsSelecionados="algunsSelecionados"
|
||||
@alternar-ordenacao="alternarOrdenacao"
|
||||
@alternar-selecao-todos="alternarSelecaoTodos"
|
||||
/>
|
||||
<div class="eli-tabela__table-wrapper">
|
||||
<table class="eli-tabela__table">
|
||||
<EliTabelaHead
|
||||
:colunas="colunasEfetivas"
|
||||
:temAcoes="temAcoes"
|
||||
:temColunasInvisiveis="temColunasInvisiveis"
|
||||
:colunaOrdenacao="colunaOrdenacao"
|
||||
:direcaoOrdenacao="direcaoOrdenacao"
|
||||
:mostrarSelecao="mostrarSelecao"
|
||||
:todosSelecionados="todosSelecionados"
|
||||
:algunsSelecionados="algunsSelecionados"
|
||||
@alternar-ordenacao="alternarOrdenacao"
|
||||
@alternar-selecao-todos="alternarSelecaoTodos"
|
||||
/>
|
||||
|
||||
<tbody v-if="carregando || Boolean(erro) || !linhas.length">
|
||||
<tr>
|
||||
<td :colspan="totalColunas" style="padding: 0;">
|
||||
<EliTabelaEstados
|
||||
:carregando="carregando"
|
||||
:erro="erro"
|
||||
:mensagemVazio="tabela.mensagemVazio"
|
||||
style="border: none; border-radius: 0;"
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
<tbody v-if="carregando || Boolean(erro) || !linhas.length">
|
||||
<tr>
|
||||
<td :colspan="totalColunas" class="eli-tabela__td--estados">
|
||||
<EliTabelaEstados
|
||||
:carregando="carregando"
|
||||
:erro="erro"
|
||||
:mensagemVazio="tabela.mensagemVazio"
|
||||
class="eli-tabela__estados-inline"
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
|
||||
<EliTabelaBody
|
||||
v-else
|
||||
:colunas="colunasEfetivas"
|
||||
:colunasInvisiveis="colunasInvisiveisEfetivas"
|
||||
:temColunasInvisiveis="temColunasInvisiveis"
|
||||
:linhasExpandidas="linhasExpandidas"
|
||||
:linhas="linhasPaginadas"
|
||||
:temAcoes="temAcoes"
|
||||
:menuAberto="menuAberto"
|
||||
:possuiAcoes="possuiAcoes"
|
||||
:toggleMenu="toggleMenu"
|
||||
:alternarLinhaExpandida="alternarLinhaExpandida"
|
||||
:mostrarSelecao="mostrarSelecao"
|
||||
:isSelecionado="isSelecionado"
|
||||
:alternarSelecao="alternarSelecao"
|
||||
/>
|
||||
</table>
|
||||
<EliTabelaBody
|
||||
v-else
|
||||
:colunas="colunasEfetivas"
|
||||
:colunasInvisiveis="colunasInvisiveisEfetivas"
|
||||
:temColunasInvisiveis="temColunasInvisiveis"
|
||||
:linhasExpandidas="linhasExpandidas"
|
||||
:linhas="linhasPaginadas"
|
||||
:temAcoes="temAcoes"
|
||||
:menuAberto="menuAberto"
|
||||
:possuiAcoes="possuiAcoes"
|
||||
:toggleMenu="toggleMenu"
|
||||
:alternarLinhaExpandida="alternarLinhaExpandida"
|
||||
:mostrarSelecao="mostrarSelecao"
|
||||
:isSelecionado="isSelecionado"
|
||||
:alternarSelecao="alternarSelecao"
|
||||
/>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<EliTabelaMenuAcoes
|
||||
ref="menuPopup"
|
||||
|
|
@ -108,28 +111,16 @@
|
|||
/**
|
||||
* EliTabela
|
||||
* Componente de tabela consultável com busca, paginação, ordenação e ações por linha.
|
||||
* O setup() é orquestrado por `useEliTabela`, que compõe os seguintes sub-composables:
|
||||
* - useEliTabelaConsulta — carregamento, paginação, ordenação, busca
|
||||
* - useEliTabelaFiltrosAvancados — filtros avançados, modal e persistência
|
||||
* - useEliTabelaSelecao — seleção múltipla de linhas
|
||||
* - useEliTabelaColunas — visibilidade, ordem e modal de colunas
|
||||
* - useEliTabelaMenu — menu de ações por linha e posicionamento de popup
|
||||
*/
|
||||
|
||||
/** Enum de códigos de resposta utilizado na consulta */
|
||||
import { codigosResposta } from "p-respostas"
|
||||
/** Dependências do Vue (Composition API) */
|
||||
import {
|
||||
computed,
|
||||
defineComponent,
|
||||
onBeforeUnmount,
|
||||
onMounted,
|
||||
type PropType,
|
||||
ref,
|
||||
watch,
|
||||
} from "vue"
|
||||
// operadores usados no tipo de configuração; o operador aplicado vem travado no filtroAvancado.
|
||||
import {
|
||||
carregarConfigColunas,
|
||||
type EliTabelaColunasConfig,
|
||||
salvarConfigColunas,
|
||||
} from "./colunasStorage"
|
||||
import { defineComponent, type PropType } from "vue"
|
||||
import "./EliTabela.css"
|
||||
import EliTabelaBody from "./EliTabelaBody.vue"
|
||||
/** Componentes auxiliares */
|
||||
import EliTabelaCabecalho from "./EliTabelaCabecalho.vue"
|
||||
import EliTabelaDebug from "./EliTabelaDebug.vue"
|
||||
import EliTabelaEstados from "./EliTabelaEstados.vue"
|
||||
|
|
@ -138,18 +129,12 @@ import EliTabelaMenuAcoes from "./EliTabelaMenuAcoes.vue"
|
|||
import EliTabelaModalColunas from "./EliTabelaModalColunas.vue"
|
||||
import EliTabelaModalFiltroAvancado from "./EliTabelaModalFiltroAvancado.vue"
|
||||
import EliTabelaPaginacao from "./EliTabelaPaginacao.vue"
|
||||
import {
|
||||
carregarFiltroAvancado,
|
||||
limparFiltroAvancado,
|
||||
salvarFiltroAvancado,
|
||||
} from "./filtroAvancadoStorage"
|
||||
/** Tipos da configuração/contrato da tabela */
|
||||
import type {
|
||||
parametrosConsulta,
|
||||
tipoEliColuna,
|
||||
tipoEliTabelaConsulta,
|
||||
tipoFiltro,
|
||||
} from "./types-eli-tabela"
|
||||
import type { tipoEliTabelaConsulta } from "./types-eli-tabela"
|
||||
import { useEliTabela } from "./useEliTabela"
|
||||
import type { FiltroBase } from "./useFiltroAvancado"
|
||||
|
||||
// biome-ignore lint/correctness/noUnusedVariables: tipo usado no template SFC no cast :filtrosBase
|
||||
type FiltroBaseModal = FiltroBase<string>
|
||||
|
||||
export default defineComponent({
|
||||
name: "EliTabela",
|
||||
|
|
@ -170,821 +155,22 @@ export default defineComponent({
|
|||
tabela: {
|
||||
// biome-ignore lint/suspicious/noExplicitAny: dynamic table
|
||||
type: Object as PropType<tipoEliTabelaConsulta<any>>,
|
||||
required: true,
|
||||
required: true as const,
|
||||
},
|
||||
/** Array de itens selecionados (v-model:selecionados) */
|
||||
selecionados: {
|
||||
type: Array as PropType<unknown[]>,
|
||||
default: () => [],
|
||||
},
|
||||
/** Ocultar painel de debug lateral (EliTabelaDebug) */
|
||||
esconderDebug: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
emits: ["update:selecionados"],
|
||||
setup(props, { emit }) {
|
||||
/** Flag para habilitar elementos de debug */
|
||||
const isDev = import.meta.env.DEV
|
||||
/** Estados de carregamento/erro e dados retornados */
|
||||
const carregando = ref(false)
|
||||
const erro = ref<string | null>(null)
|
||||
const linhas = ref<unknown[]>([])
|
||||
const quantidade = ref<number>(0)
|
||||
|
||||
/** Controle de visibilidade das ações por linha */
|
||||
const acoesVisiveis = ref<boolean[][]>([])
|
||||
/** Estado do menu de ações (aberto, elemento e posição) */
|
||||
const menuAberto = ref<number | null>(null)
|
||||
// O componente EliTabelaMenuAcoes expõe `menuEl` (ref do elemento <ul>)
|
||||
const menuPopup = ref<{ menuEl: { value: HTMLElement | null } } | null>(
|
||||
null,
|
||||
)
|
||||
const menuPopupPos = ref({ top: 0, left: 0 })
|
||||
|
||||
/** Filtros e ordenação */
|
||||
const valorBusca = ref<string>("")
|
||||
const paginaAtual = ref(1)
|
||||
const colunaOrdenacao = ref<string | null>(null)
|
||||
const direcaoOrdenacao = ref<"asc" | "desc">("asc")
|
||||
|
||||
/** Filtro avançado (config + estado modal) */
|
||||
const modalFiltroAberto = ref(false)
|
||||
type LinhaFiltroUI = {
|
||||
chave: string
|
||||
// biome-ignore lint/suspicious/noExplicitAny: dynamic value
|
||||
valor: any
|
||||
}
|
||||
const filtrosUi = ref<Array<LinhaFiltroUI>>(
|
||||
// biome-ignore lint/suspicious/noExplicitAny: dynamic load
|
||||
carregarFiltroAvancado<any>(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
|
||||
// Se o usuário estiver usando filtro avançado, a busca deixa de ter efeito.
|
||||
// Mantemos a regra combinatória (busca tem prioridade), então limpamos a busca.
|
||||
valorBusca.value = ""
|
||||
if (paginaAtual.value !== 1) paginaAtual.value = 1
|
||||
else void carregar()
|
||||
}
|
||||
|
||||
// biome-ignore lint/suspicious/noExplicitAny: dynamic ui
|
||||
function salvarFiltrosAvancados(novo: any[]) {
|
||||
// biome-ignore lint/suspicious/noExplicitAny: dynamic ui
|
||||
filtrosUi.value = (novo ?? []) as any
|
||||
// biome-ignore lint/suspicious/noExplicitAny: dynamic storage
|
||||
salvarFiltroAvancado(props.tabela.nome, (novo ?? []) as any)
|
||||
modalFiltroAberto.value = false
|
||||
// Ao aplicar filtros, limpamos a busca para garantir que os filtros sejam efetivos.
|
||||
// (busca tem prioridade sobre filtros)
|
||||
valorBusca.value = ""
|
||||
if (paginaAtual.value !== 1) paginaAtual.value = 1
|
||||
else void carregar()
|
||||
}
|
||||
|
||||
// biome-ignore lint/suspicious/noExplicitAny: generic
|
||||
const filtrosAvancadosAtivos = computed<tipoFiltro<any>[]>(() => {
|
||||
const base = props.tabela.filtroAvancado ?? []
|
||||
|
||||
return (
|
||||
(filtrosUi.value ?? [])
|
||||
.filter((f) => f && f.chave !== undefined)
|
||||
.map((f) => {
|
||||
const b = base.find((x) => String(x.chave) === String(f.chave))
|
||||
if (!b) return null
|
||||
|
||||
// O filtro já vem pronto da função definida na configuração
|
||||
return b.filtro(f.valor)
|
||||
})
|
||||
// biome-ignore lint/suspicious/noExplicitAny: generic
|
||||
.filter(Boolean) as tipoFiltro<any>[]
|
||||
)
|
||||
})
|
||||
|
||||
/** Alias reativo da prop tabela */
|
||||
const tabela = computed(() => props.tabela)
|
||||
|
||||
/** Seleção Múltipla */
|
||||
const mostrarSelecao = computed(() => Boolean(props.tabela.mostrar_selecao))
|
||||
const chaveSelecao = computed(
|
||||
() => props.tabela.chave_selecao as string | undefined,
|
||||
)
|
||||
|
||||
function obterChaveLinha(linha: unknown): unknown {
|
||||
if (chaveSelecao.value && linha && typeof linha === "object") {
|
||||
return (linha as Record<string, unknown>)[chaveSelecao.value]
|
||||
}
|
||||
return linha
|
||||
}
|
||||
|
||||
function isSelecionado(linha: unknown): boolean {
|
||||
const chave = obterChaveLinha(linha)
|
||||
if (chaveSelecao.value) {
|
||||
return props.selecionados.some((s) => obterChaveLinha(s) === chave)
|
||||
}
|
||||
return props.selecionados.includes(linha)
|
||||
}
|
||||
|
||||
function alternarSelecao(linha: unknown) {
|
||||
const atual = [...props.selecionados]
|
||||
const chave = obterChaveLinha(linha)
|
||||
|
||||
let index = -1
|
||||
if (chaveSelecao.value) {
|
||||
index = atual.findIndex((s) => obterChaveLinha(s) === chave)
|
||||
} else {
|
||||
index = atual.indexOf(linha)
|
||||
}
|
||||
|
||||
if (index === -1) {
|
||||
atual.push(linha)
|
||||
} else {
|
||||
atual.splice(index, 1)
|
||||
}
|
||||
emit("update:selecionados", atual)
|
||||
}
|
||||
|
||||
const todosSelecionados = computed(() => {
|
||||
if (linhasPaginadas.value.length === 0) return false
|
||||
return linhasPaginadas.value.every((linha) => isSelecionado(linha))
|
||||
})
|
||||
|
||||
const algunsSelecionados = computed(() => {
|
||||
if (linhasPaginadas.value.length === 0) return false
|
||||
return (
|
||||
linhasPaginadas.value.some((linha) => isSelecionado(linha)) &&
|
||||
!todosSelecionados.value
|
||||
)
|
||||
})
|
||||
|
||||
function alternarSelecaoTodos(forcarMarcar?: boolean) {
|
||||
const marcarTodos = forcarMarcar ?? !todosSelecionados.value
|
||||
let atual = [...props.selecionados]
|
||||
|
||||
if (marcarTodos) {
|
||||
// Adiciona os que faltam na página atual
|
||||
for (const linha of linhasPaginadas.value) {
|
||||
if (!isSelecionado(linha)) {
|
||||
atual.push(linha)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Remove os da página atual
|
||||
const chavesPagina = linhasPaginadas.value.map(obterChaveLinha)
|
||||
if (chaveSelecao.value) {
|
||||
atual = atual.filter(
|
||||
(s) => !chavesPagina.includes(obterChaveLinha(s)),
|
||||
)
|
||||
} else {
|
||||
atual = atual.filter((s) => !chavesPagina.includes(s))
|
||||
}
|
||||
}
|
||||
emit("update:selecionados", atual)
|
||||
}
|
||||
|
||||
/** Exibição da busca e ações do cabeçalho */
|
||||
const exibirBusca = computed(() =>
|
||||
Boolean(props.tabela.mostrarCaixaDeBusca),
|
||||
)
|
||||
const acoesSuperiores = computed(() =>
|
||||
(props.tabela.acoesTabela ?? []).filter((a) => a.posicao === "superior"),
|
||||
)
|
||||
const acoesInferiores = computed(() =>
|
||||
(props.tabela.acoesTabela ?? []).filter((a) => a.posicao === "inferior"),
|
||||
)
|
||||
const temAcoesSuperiores = computed(() => acoesSuperiores.value.length > 0)
|
||||
const temAcoesInferiores = computed(() => acoesInferiores.value.length > 0)
|
||||
|
||||
const parametrosConsultaAtuais = computed(() => {
|
||||
const limite = Math.max(1, registrosPorConsulta.value)
|
||||
const offset = (paginaAtual.value - 1) * limite
|
||||
|
||||
// biome-ignore lint/suspicious/noExplicitAny: dynamic params
|
||||
const params: parametrosConsulta<any> = {
|
||||
offSet: offset,
|
||||
limit: limite,
|
||||
}
|
||||
|
||||
const busca = (valorBusca.value ?? "").trim()
|
||||
if (busca) {
|
||||
params.texto_busca = busca
|
||||
} else {
|
||||
const filtros = filtrosAvancadosAtivos.value
|
||||
if (filtros.length) params.filtros = filtros
|
||||
}
|
||||
|
||||
if (colunaOrdenacao.value) {
|
||||
params.coluna_ordem = colunaOrdenacao.value
|
||||
params.direcao_ordem = direcaoOrdenacao.value
|
||||
}
|
||||
|
||||
return {
|
||||
...params,
|
||||
atualizarConsulta: async () => {
|
||||
await carregar()
|
||||
},
|
||||
// biome-ignore lint/suspicious/noExplicitAny: dynamic list update
|
||||
editarLista: async (novaLista: any[]) => {
|
||||
linhas.value = novaLista
|
||||
return novaLista
|
||||
},
|
||||
}
|
||||
})
|
||||
|
||||
/** Colunas: visibilidade/ordem com persistência */
|
||||
const modalColunasAberto = ref(false)
|
||||
const configColunas = ref<EliTabelaColunasConfig>(
|
||||
carregarConfigColunas(props.tabela.nome),
|
||||
)
|
||||
|
||||
/** Linhas expandidas (para exibir colunas invisíveis) */
|
||||
const linhasExpandidas = ref<Record<number, boolean>>({})
|
||||
|
||||
const rotulosColunas = computed(() =>
|
||||
props.tabela.colunas.map((c) => c.rotulo),
|
||||
)
|
||||
|
||||
const colunasInvisiveisEfetivas = computed(() => {
|
||||
// biome-ignore lint/suspicious/noExplicitAny: dynamic column
|
||||
const colunas = props.tabela.colunas as Array<tipoEliColuna<any>>
|
||||
|
||||
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 ou derivada do default) e adiciona novas ao final
|
||||
const ordemSalva = invisiveisBaseRotulos
|
||||
// biome-ignore lint/suspicious/noExplicitAny: dynamic map
|
||||
const mapa = new Map<string, tipoEliColuna<any>>()
|
||||
for (const c of base) {
|
||||
if (!mapa.has(c.rotulo)) mapa.set(c.rotulo, c)
|
||||
}
|
||||
|
||||
// biome-ignore lint/suspicious/noExplicitAny: dynamic column
|
||||
const ordenadas: Array<tipoEliColuna<any>> = []
|
||||
for (const r of ordemSalva) {
|
||||
const c = mapa.get(r)
|
||||
if (c) ordenadas.push(c)
|
||||
}
|
||||
for (const c of base) {
|
||||
if (!ordenadas.includes(c)) ordenadas.push(c)
|
||||
}
|
||||
|
||||
return ordenadas
|
||||
})
|
||||
|
||||
const temColunasInvisiveis = computed(
|
||||
() => colunasInvisiveisEfetivas.value.length > 0,
|
||||
)
|
||||
|
||||
const colunasEfetivas = computed(() => {
|
||||
const colunas = props.tabela.colunas
|
||||
const todosRotulos = rotulosColunas.value
|
||||
|
||||
const configTemDados =
|
||||
(configColunas.value.visiveis?.length ?? 0) > 0 ||
|
||||
(configColunas.value.invisiveis?.length ?? 0) > 0
|
||||
|
||||
const invisiveisBaseRotulos = configTemDados
|
||||
? (configColunas.value.invisiveis ?? [])
|
||||
: // biome-ignore lint/suspicious/noExplicitAny: dynamic column
|
||||
(props.tabela.colunas as Array<tipoEliColuna<any>>)
|
||||
.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 = configTemDados
|
||||
? (configColunas.value.visiveis ?? [])
|
||||
: []
|
||||
const ordemFinal: string[] = []
|
||||
|
||||
for (const r of ordemSalva) {
|
||||
if (visiveisSet.has(r)) ordemFinal.push(r)
|
||||
}
|
||||
for (const r of visiveisBaseRotulos) {
|
||||
if (!ordemFinal.includes(r)) ordemFinal.push(r)
|
||||
}
|
||||
|
||||
// mapeia rótulo -> coluna, preservando duplicatas (se existirem) pelo primeiro match.
|
||||
// OBS: pressupoe rotulo unico; se repetir, comportamento fica indefinido.
|
||||
// biome-ignore lint/suspicious/noExplicitAny: dynamic map
|
||||
const mapa = new Map<string, any>()
|
||||
for (const c of colunas) {
|
||||
if (!mapa.has(c.rotulo)) mapa.set(c.rotulo, c)
|
||||
}
|
||||
|
||||
return ordemFinal.map((r) => mapa.get(r)).filter(Boolean)
|
||||
})
|
||||
|
||||
const totalColunas = computed(() => {
|
||||
return (
|
||||
colunasEfetivas.value.length +
|
||||
(temAcoes.value ? 1 : 0) +
|
||||
(temColunasInvisiveis.value ? 1 : 0)
|
||||
)
|
||||
})
|
||||
|
||||
function abrirModalColunas() {
|
||||
modalColunasAberto.value = true
|
||||
}
|
||||
|
||||
function fecharModalColunas() {
|
||||
modalColunasAberto.value = false
|
||||
}
|
||||
|
||||
function salvarModalColunas(cfg: EliTabelaColunasConfig) {
|
||||
configColunas.value = cfg
|
||||
salvarConfigColunas(props.tabela.nome, cfg)
|
||||
modalColunasAberto.value = false
|
||||
|
||||
// ao mudar colunas, fecha detalhes expandidos
|
||||
linhasExpandidas.value = {}
|
||||
}
|
||||
|
||||
function alternarLinhaExpandida(indice: number) {
|
||||
const atual = Boolean(linhasExpandidas.value[indice])
|
||||
linhasExpandidas.value = {
|
||||
...linhasExpandidas.value,
|
||||
[indice]: !atual,
|
||||
}
|
||||
}
|
||||
|
||||
/** Registros por consulta (normaliza para inteiro positivo) */
|
||||
const registrosPorConsulta = computed(() => {
|
||||
const valor = props.tabela.registros_por_consulta
|
||||
if (typeof valor === "number" && valor > 0) {
|
||||
return Math.floor(valor)
|
||||
}
|
||||
return 10
|
||||
})
|
||||
|
||||
/** Total de páginas calculado com base no total retornado pela API */
|
||||
const totalPaginas = computed(() => {
|
||||
const limite = registrosPorConsulta.value
|
||||
if (!limite || limite <= 0) return 1
|
||||
const total = quantidade.value ?? 0
|
||||
if (!total) return 1
|
||||
return Math.max(1, Math.ceil(total / limite))
|
||||
})
|
||||
|
||||
/** As linhas já vêm paginadas do backend */
|
||||
const linhasPaginadas = computed(() => linhas.value ?? [])
|
||||
|
||||
/** Quantidade exibida é a quantidade total retornada pela consulta */
|
||||
const quantidadeFiltrada = computed(() => quantidade.value ?? 0)
|
||||
|
||||
/** 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
|
||||
|
||||
/** Calcula a posição do menu de ações na viewport */
|
||||
function atualizarPosicaoMenu(anchor: HTMLElement) {
|
||||
const rect = anchor.getBoundingClientRect()
|
||||
const gap = 8
|
||||
|
||||
// Alinha no canto inferior direito do botão.
|
||||
// Se estourar a tela para baixo, abre para cima.
|
||||
const alturaMenu = menuPopup.value?.menuEl?.value?.offsetHeight ?? 0
|
||||
const larguraMenu = menuPopup.value?.menuEl?.value?.offsetWidth ?? 180
|
||||
|
||||
let top = rect.bottom + gap
|
||||
const left = rect.right - larguraMenu
|
||||
|
||||
if (alturaMenu && top + alturaMenu > window.innerHeight - gap) {
|
||||
top = rect.top - gap - alturaMenu
|
||||
}
|
||||
|
||||
menuPopupPos.value = {
|
||||
top: Math.max(gap, Math.round(top)),
|
||||
left: Math.max(gap, Math.round(left)),
|
||||
}
|
||||
}
|
||||
|
||||
/** Fecha o menu quando ocorre clique fora */
|
||||
function handleClickFora(evento: MouseEvent) {
|
||||
if (menuAberto.value === null) return
|
||||
|
||||
const alvo = evento.target as Node
|
||||
if (menuPopup.value?.menuEl?.value?.contains(alvo)) return
|
||||
|
||||
if (import.meta.env.DEV) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log("[EliTabela] click fora => fechar menu", {
|
||||
menuAberto: menuAberto.value,
|
||||
})
|
||||
}
|
||||
|
||||
menuAberto.value = null
|
||||
}
|
||||
|
||||
/** Alterna ordenação e recarrega os dados */
|
||||
function alternarOrdenacao(chave?: string) {
|
||||
if (!chave) return
|
||||
|
||||
if (colunaOrdenacao.value === chave) {
|
||||
direcaoOrdenacao.value =
|
||||
direcaoOrdenacao.value === "asc" ? "desc" : "asc"
|
||||
void carregar()
|
||||
return
|
||||
}
|
||||
|
||||
colunaOrdenacao.value = chave
|
||||
direcaoOrdenacao.value = "asc"
|
||||
if (paginaAtual.value !== 1) {
|
||||
paginaAtual.value = 1
|
||||
} else {
|
||||
void carregar()
|
||||
}
|
||||
}
|
||||
|
||||
/** Atualiza a busca e reinicia paginação, se necessário */
|
||||
function atualizarBusca(texto: string) {
|
||||
if (valorBusca.value === texto) return
|
||||
|
||||
valorBusca.value = texto
|
||||
if (paginaAtual.value !== 1) {
|
||||
paginaAtual.value = 1
|
||||
} else {
|
||||
void carregar()
|
||||
}
|
||||
}
|
||||
|
||||
/** Navega para a página solicitada com limites */
|
||||
function irParaPagina(pagina: number) {
|
||||
const alvo = Math.min(Math.max(1, pagina), totalPaginas.value)
|
||||
if (alvo !== paginaAtual.value) {
|
||||
paginaAtual.value = alvo
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Lista ações visíveis por linha, respeitando regras sync/async de `exibir`.
|
||||
*/
|
||||
function acoesDisponiveisPorLinha(i: number) {
|
||||
const acoesLinha = props.tabela.acoesLinha ?? []
|
||||
const visibilidade = acoesVisiveis.value[i] ?? []
|
||||
|
||||
return acoesLinha
|
||||
.map((acao, indice) => {
|
||||
const fallbackVisivel =
|
||||
acao.exibir === undefined
|
||||
? true
|
||||
: typeof acao.exibir === "boolean"
|
||||
? acao.exibir
|
||||
: false
|
||||
|
||||
return {
|
||||
acao,
|
||||
indice,
|
||||
visivel: visibilidade[indice] ?? fallbackVisivel,
|
||||
}
|
||||
})
|
||||
.filter((item) => item.visivel)
|
||||
}
|
||||
|
||||
/** Informa se a linha possui ações disponíveis */
|
||||
function possuiAcoes(i: number) {
|
||||
return acoesDisponiveisPorLinha(i).length > 0
|
||||
}
|
||||
|
||||
/** Abre/fecha o menu de ações da linha e posiciona o popup */
|
||||
function toggleMenu(i: number, evento?: MouseEvent) {
|
||||
if (!possuiAcoes(i)) return
|
||||
|
||||
if (import.meta.env.DEV) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log("[EliTabela] toggleMenu (antes)", {
|
||||
i,
|
||||
atual: menuAberto.value,
|
||||
})
|
||||
}
|
||||
|
||||
if (menuAberto.value === i) {
|
||||
menuAberto.value = null
|
||||
|
||||
if (import.meta.env.DEV) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log("[EliTabela] toggleMenu => fechou", { i })
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
menuAberto.value = i
|
||||
|
||||
if (import.meta.env.DEV) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log("[EliTabela] toggleMenu => abriu", { i })
|
||||
}
|
||||
|
||||
// posiciona assim que abrir
|
||||
const anchor = (evento?.currentTarget as HTMLElement | null) ?? null
|
||||
if (anchor) {
|
||||
// primeiro posicionamento (antes do menu medir)
|
||||
atualizarPosicaoMenu(anchor)
|
||||
// reposiciona no próximo frame para pegar altura real
|
||||
requestAnimationFrame(() => atualizarPosicaoMenu(anchor))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Executa consulta, aplica filtros e resolve visibilidade de ações.
|
||||
* Usa o contador sequencial para ignorar respostas antigas.
|
||||
*/
|
||||
async function carregar() {
|
||||
const idCarregamento = ++carregamentoSequencial
|
||||
carregando.value = true
|
||||
erro.value = null
|
||||
|
||||
try {
|
||||
acoesVisiveis.value = []
|
||||
menuAberto.value = null
|
||||
linhasExpandidas.value = {}
|
||||
|
||||
const limite = Math.max(1, registrosPorConsulta.value)
|
||||
const offset = (paginaAtual.value - 1) * limite
|
||||
|
||||
const parametrosConsulta: {
|
||||
// biome-ignore lint/suspicious/noExplicitAny: generic
|
||||
filtros?: tipoFiltro<any>[]
|
||||
coluna_ordem?: never
|
||||
direcao_ordem?: "asc" | "desc"
|
||||
offSet: number
|
||||
limit: number
|
||||
texto_busca?: string
|
||||
} = {
|
||||
offSet: offset,
|
||||
limit: limite,
|
||||
}
|
||||
|
||||
// Regra combinatória definida: busca tem prioridade.
|
||||
const busca = (valorBusca.value ?? "").trim()
|
||||
if (busca) {
|
||||
parametrosConsulta.texto_busca = busca
|
||||
} else {
|
||||
const filtros = filtrosAvancadosAtivos.value
|
||||
if (filtros.length) parametrosConsulta.filtros = filtros
|
||||
}
|
||||
|
||||
if (colunaOrdenacao.value) {
|
||||
parametrosConsulta.coluna_ordem = colunaOrdenacao.value as never
|
||||
parametrosConsulta.direcao_ordem = direcaoOrdenacao.value
|
||||
}
|
||||
|
||||
if (import.meta.env.DEV) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log("[EliTabela] consulta(parametros)", parametrosConsulta)
|
||||
}
|
||||
|
||||
const tabelaConfig = props.tabela
|
||||
const res = await tabelaConfig.consulta(parametrosConsulta)
|
||||
|
||||
if (idCarregamento !== carregamentoSequencial) return
|
||||
|
||||
if (res.cod !== codigosResposta.sucesso) {
|
||||
linhas.value = []
|
||||
quantidade.value = 0
|
||||
erro.value = res.mensagem
|
||||
return
|
||||
}
|
||||
|
||||
const valores = res.valor?.valores ?? []
|
||||
// biome-ignore lint/suspicious/noExplicitAny: dynamic response
|
||||
const total = (res.valor as any)?.quantidade ?? valores.length
|
||||
|
||||
linhas.value = valores
|
||||
quantidade.value = Number(total) || 0
|
||||
|
||||
const totalPaginasRecalculado = Math.max(
|
||||
1,
|
||||
Math.ceil((quantidade.value || 0) / limite),
|
||||
)
|
||||
if (paginaAtual.value > totalPaginasRecalculado)
|
||||
paginaAtual.value = totalPaginasRecalculado
|
||||
|
||||
const acoesLinhaConfiguradas = tabelaConfig.acoesLinha ?? []
|
||||
if (!acoesLinhaConfiguradas.length) {
|
||||
acoesVisiveis.value = []
|
||||
return
|
||||
}
|
||||
|
||||
const preResultado = valores.map(() =>
|
||||
acoesLinhaConfiguradas.map((acao) => {
|
||||
if (acao.exibir === undefined) return true
|
||||
if (typeof acao.exibir === "boolean") return acao.exibir
|
||||
return false
|
||||
}),
|
||||
)
|
||||
|
||||
acoesVisiveis.value = preResultado
|
||||
|
||||
const visibilidade = await Promise.all(
|
||||
valores.map(async (linha) =>
|
||||
Promise.all(
|
||||
acoesLinhaConfiguradas.map(async (acao) => {
|
||||
if (acao.exibir === undefined) return true
|
||||
if (typeof acao.exibir === "boolean") return acao.exibir
|
||||
|
||||
try {
|
||||
const resultado = acao.exibir(linha as never)
|
||||
return Boolean(await Promise.resolve(resultado))
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}),
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
if (idCarregamento === carregamentoSequencial) {
|
||||
acoesVisiveis.value = visibilidade
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("[EliTabela] Erro capturado em carregar():", e)
|
||||
if (idCarregamento !== carregamentoSequencial) return
|
||||
|
||||
linhas.value = []
|
||||
quantidade.value = 0
|
||||
erro.value = e instanceof Error ? e.message : "Erro ao carregar dados."
|
||||
} finally {
|
||||
if (idCarregamento === carregamentoSequencial) {
|
||||
carregando.value = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Ciclo de vida: registra listener global e carrega dados iniciais */
|
||||
onMounted(() => {
|
||||
document.addEventListener("click", handleClickFora)
|
||||
void carregar()
|
||||
})
|
||||
|
||||
/** Ciclo de vida: remove listener global */
|
||||
onBeforeUnmount(() => {
|
||||
document.removeEventListener("click", handleClickFora)
|
||||
})
|
||||
|
||||
/** Watch: ao desabilitar busca, limpa termo e recarrega */
|
||||
watch(
|
||||
() => props.tabela.mostrarCaixaDeBusca,
|
||||
(mostrar) => {
|
||||
if (!mostrar && valorBusca.value) {
|
||||
valorBusca.value = ""
|
||||
if (paginaAtual.value !== 1) {
|
||||
paginaAtual.value = 1
|
||||
} else {
|
||||
void carregar()
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
/** Watch: mudança de página dispara nova consulta */
|
||||
watch(paginaAtual, (nova, antiga) => {
|
||||
if (nova !== antiga) void carregar()
|
||||
})
|
||||
|
||||
/** Watch: troca de configuração reseta estados e recarrega */
|
||||
watch(
|
||||
() => props.tabela,
|
||||
() => {
|
||||
menuAberto.value = null
|
||||
colunaOrdenacao.value = null
|
||||
direcaoOrdenacao.value = "asc"
|
||||
valorBusca.value = ""
|
||||
modalColunasAberto.value = false
|
||||
modalFiltroAberto.value = false
|
||||
configColunas.value = carregarConfigColunas(props.tabela.nome)
|
||||
// biome-ignore lint/suspicious/noExplicitAny: dynamic reload
|
||||
filtrosUi.value = carregarFiltroAvancado<any>(props.tabela.nome) as any
|
||||
linhasExpandidas.value = {}
|
||||
if (paginaAtual.value !== 1) {
|
||||
paginaAtual.value = 1
|
||||
} else {
|
||||
void carregar()
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
/** Watch: alteração do limite de registros reinicia paginação */
|
||||
watch(
|
||||
() => props.tabela.registros_por_consulta,
|
||||
() => {
|
||||
if (paginaAtual.value !== 1) {
|
||||
paginaAtual.value = 1
|
||||
} else {
|
||||
void carregar()
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
/** Watch: mudança nas linhas fecha o menu aberto */
|
||||
watch(linhas, () => {
|
||||
menuAberto.value = null
|
||||
linhasExpandidas.value = {}
|
||||
})
|
||||
|
||||
/** Exposição para o template (state, computed, helpers e actions) */
|
||||
return {
|
||||
// state
|
||||
isDev,
|
||||
tabela,
|
||||
carregando,
|
||||
erro,
|
||||
linhas,
|
||||
linhasPaginadas,
|
||||
filtrosAvancadosAtivos,
|
||||
quantidadeFiltrada,
|
||||
quantidade,
|
||||
menuAberto,
|
||||
valorBusca,
|
||||
paginaAtual,
|
||||
colunaOrdenacao,
|
||||
direcaoOrdenacao,
|
||||
totalPaginas,
|
||||
registrosPorConsulta,
|
||||
|
||||
// computed
|
||||
exibirBusca,
|
||||
exibirFiltroAvancado,
|
||||
acoesSuperiores,
|
||||
temAcoesSuperiores,
|
||||
acoesInferiores,
|
||||
temAcoesInferiores,
|
||||
parametrosConsultaAtuais,
|
||||
temAcoes,
|
||||
totalColunas,
|
||||
colunasEfetivas,
|
||||
rotulosColunas,
|
||||
modalColunasAberto,
|
||||
configColunas,
|
||||
|
||||
temColunasInvisiveis,
|
||||
colunasInvisiveisEfetivas,
|
||||
linhasExpandidas,
|
||||
|
||||
// selecao
|
||||
mostrarSelecao,
|
||||
isSelecionado,
|
||||
alternarSelecao,
|
||||
todosSelecionados,
|
||||
algunsSelecionados,
|
||||
alternarSelecaoTodos,
|
||||
|
||||
abrirModalColunas,
|
||||
abrirModalFiltro,
|
||||
fecharModalColunas,
|
||||
salvarModalColunas,
|
||||
|
||||
modalFiltroAberto,
|
||||
filtrosUi,
|
||||
salvarFiltrosAvancados,
|
||||
limparFiltrosAvancados,
|
||||
fecharModalFiltro,
|
||||
|
||||
alternarLinhaExpandida,
|
||||
|
||||
// actions
|
||||
alternarOrdenacao,
|
||||
atualizarBusca,
|
||||
irParaPagina,
|
||||
acoesDisponiveisPorLinha,
|
||||
possuiAcoes,
|
||||
toggleMenu,
|
||||
|
||||
// popup
|
||||
menuPopup,
|
||||
menuPopupPos,
|
||||
}
|
||||
return useEliTabela(props, emit)
|
||||
},
|
||||
})
|
||||
</script>
|
||||
|
||||
<style src="./EliTabela.css"></style>
|
||||
|
|
|
|||
|
|
@ -102,12 +102,12 @@ export default defineComponent({
|
|||
props: {
|
||||
colunas: {
|
||||
// biome-ignore lint/suspicious/noExplicitAny: dynamic column
|
||||
type: Array as PropType<Array<tipoEliColuna<any>>>,
|
||||
type: Array as PropType<tipoEliColuna<any>[]>,
|
||||
required: true,
|
||||
},
|
||||
colunasInvisiveis: {
|
||||
// biome-ignore lint/suspicious/noExplicitAny: dynamic column
|
||||
type: Array as PropType<Array<tipoEliColuna<any>>>,
|
||||
type: Array as PropType<tipoEliColuna<any>[]>,
|
||||
required: true,
|
||||
},
|
||||
temColunasInvisiveis: {
|
||||
|
|
@ -119,7 +119,7 @@ export default defineComponent({
|
|||
required: true,
|
||||
},
|
||||
linhas: {
|
||||
type: Array as PropType<Array<unknown>>,
|
||||
type: Array as PropType<unknown[]>,
|
||||
required: true,
|
||||
},
|
||||
temAcoes: {
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@
|
|||
class="eli-tabela__acoes-cabecalho-botao eli-tabela__acoes-cabecalho-botao--colunas"
|
||||
@click="emitColunas"
|
||||
>
|
||||
<Columns3 :size="15" :stroke-width="2" />
|
||||
Colunas
|
||||
</button>
|
||||
|
||||
|
|
@ -17,6 +18,7 @@
|
|||
class="eli-tabela__acoes-cabecalho-botao eli-tabela__acoes-cabecalho-botao--filtro"
|
||||
@click="emitFiltroAvancado"
|
||||
>
|
||||
<Filter :size="15" :stroke-width="2" />
|
||||
Filtro
|
||||
</button>
|
||||
<EliTabelaCaixaDeBusca :modelo="valorBusca" @buscar="emitBuscar" />
|
||||
|
|
@ -46,12 +48,13 @@
|
|||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { Columns3, Filter } from "@lucide/vue"
|
||||
import { computed, defineComponent, type PropType } from "vue"
|
||||
import EliTabelaCaixaDeBusca from "./EliTabelaCaixaDeBusca.vue"
|
||||
|
||||
export default defineComponent({
|
||||
name: "EliTabelaCabecalho",
|
||||
components: { EliTabelaCaixaDeBusca },
|
||||
components: { Columns3, EliTabelaCaixaDeBusca, Filter },
|
||||
props: {
|
||||
exibirBusca: {
|
||||
type: Boolean,
|
||||
|
|
|
|||
|
|
@ -82,7 +82,7 @@ export default defineComponent({
|
|||
border-radius: var(--eli-tabela-cabecalho-controle-radius, 8px);
|
||||
border: 1px solid rgba(15, 23, 42, 0.15);
|
||||
overflow: hidden;
|
||||
background: #fff;
|
||||
background: var(--cor-eli-superficie);
|
||||
|
||||
/* Herda do container do cabeçalho (.eli-tabela__cabecalho) quando presente. */
|
||||
height: var(--eli-tabela-cabecalho-controle-altura, 34px);
|
||||
|
|
@ -104,7 +104,7 @@ export default defineComponent({
|
|||
}
|
||||
|
||||
.eli-tabela__busca-input::placeholder {
|
||||
color: rgba(107, 114, 128, 0.85);
|
||||
color: var(--cor-eli-neutro);
|
||||
}
|
||||
|
||||
.eli-tabela__busca-botao {
|
||||
|
|
@ -112,8 +112,8 @@ export default defineComponent({
|
|||
align-items: center;
|
||||
justify-content: center;
|
||||
border: none;
|
||||
background: rgba(37, 99, 235, 0.12);
|
||||
color: rgba(37, 99, 235, 0.95);
|
||||
background: var(--cor-eli-primaria-bg);
|
||||
color: var(--cor-eli-primaria-escura);
|
||||
height: 100%;
|
||||
padding: 0 12px;
|
||||
cursor: pointer;
|
||||
|
|
@ -126,12 +126,12 @@ export default defineComponent({
|
|||
|
||||
.eli-tabela__busca-botao:hover,
|
||||
.eli-tabela__busca-botao:focus-visible {
|
||||
background: rgba(37, 99, 235, 0.2);
|
||||
color: rgba(37, 99, 235, 1);
|
||||
background: var(--cor-eli-primaria-clara);
|
||||
color: var(--cor-eli-primaria-escura);
|
||||
}
|
||||
|
||||
.eli-tabela__busca-botao:focus-visible {
|
||||
outline: 2px solid rgba(37, 99, 235, 0.35);
|
||||
outline: 2px solid var(--cor-eli-primaria-clara);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -2,17 +2,21 @@
|
|||
<!-- Debug visual (somente DEV): expõe estados internos úteis para testes e diagnóstico -->
|
||||
<div
|
||||
v-if="isDev"
|
||||
style="position: fixed; left: 8px; bottom: 8px; z-index: 999999; background: rgba(185,28,28,0.9); color: #fff; padding: 6px 10px; border-radius: 8px; font-size: 12px; max-width: 500px;"
|
||||
style="position: fixed; right: 0; top: 0; bottom: 0; width: 360px; z-index: 999999; background: rgba(20, 20, 20, 0.95); color: #32CD32; padding: 20px 16px; font-family: 'Courier New', Courier, monospace; font-size: 12px; overflow-y: auto; border-left: 1px solid #333; display: flex; flex-direction: column; gap: 6px;"
|
||||
>
|
||||
<div><b>EliTabela debug</b></div>
|
||||
<div>menuAberto: {{ menuAberto }}</div>
|
||||
<div>menuPos: top={{ menuPopupPos.top }}, left={{ menuPopupPos.left }}</div>
|
||||
<slot />
|
||||
<div style="font-weight: bold; color: #fff; margin-bottom: 8px; text-transform: uppercase; letter-spacing: 1px; border-bottom: 1px solid #333; padding-bottom: 8px;">
|
||||
EliTabela // Log Terminal
|
||||
</div>
|
||||
<div style="color: #bbb;">menuAberto: <span style="color: #fff;">{{ menuAberto }}</span></div>
|
||||
<div style="color: #bbb;">menuPos: top=<span style="color: #fff;">{{ menuPopupPos.top }}</span>, left=<span style="color: #fff;">{{ menuPopupPos.left }}</span></div>
|
||||
<div style="margin-top: 8px; white-space: pre-wrap; word-break: break-all;">
|
||||
<slot />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, type PropType } from "vue"
|
||||
import { defineComponent, onMounted, onUnmounted, type PropType, watch } from "vue"
|
||||
|
||||
export default defineComponent({
|
||||
name: "EliTabelaDebug",
|
||||
|
|
@ -30,5 +34,39 @@ export default defineComponent({
|
|||
required: true,
|
||||
},
|
||||
},
|
||||
setup(props) {
|
||||
const applyStyle = () => {
|
||||
let styleTag = document.getElementById("eli-tabela-debug-style")
|
||||
if (props.isDev) {
|
||||
if (!styleTag) {
|
||||
styleTag = document.createElement("style")
|
||||
styleTag.id = "eli-tabela-debug-style"
|
||||
styleTag.textContent = `
|
||||
html, body {
|
||||
overflow-x: auto !important;
|
||||
}
|
||||
#app, .v-application__wrap, .v-application {
|
||||
padding-right: 360px !important;
|
||||
box-sizing: border-box !important;
|
||||
}
|
||||
.v-container, .stack {
|
||||
overflow-x: auto !important;
|
||||
max-width: 100% !important;
|
||||
}
|
||||
`
|
||||
document.head.appendChild(styleTag)
|
||||
}
|
||||
} else {
|
||||
if (styleTag) styleTag.remove()
|
||||
}
|
||||
}
|
||||
|
||||
watch(() => props.isDev, applyStyle)
|
||||
onMounted(applyStyle)
|
||||
onUnmounted(() => {
|
||||
const styleTag = document.getElementById("eli-tabela-debug-style")
|
||||
if (styleTag) styleTag.remove()
|
||||
})
|
||||
},
|
||||
})
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@ export default defineComponent({
|
|||
},
|
||||
colunasInvisiveis: {
|
||||
// biome-ignore lint/suspicious/noExplicitAny: dynamic column
|
||||
type: Array as PropType<Array<tipoEliColuna<any>>>,
|
||||
type: Array as PropType<tipoEliColuna<any>[]>,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
|
|
|
|||
|
|
@ -1,26 +1,33 @@
|
|||
<template>
|
||||
<!-- Estado de carregamento: consulta em andamento -->
|
||||
<div v-if="carregando" class="eli-tabela eli-tabela--carregando" aria-busy="true">
|
||||
Carregando...
|
||||
<LoaderCircle class="eli-tabela__estado-icone eli-tabela__estado-icone--spin" :size="22" :stroke-width="2" />
|
||||
<span>Carregando…</span>
|
||||
</div>
|
||||
|
||||
<!-- Estado de erro: mostra mensagem retornada pela consulta -->
|
||||
<div v-else-if="erro" class="eli-tabela eli-tabela--erro" role="alert">
|
||||
<div class="eli-tabela__erro-titulo">Erro</div>
|
||||
<div class="eli-tabela__erro-mensagem">{{ erro }}</div>
|
||||
<AlertCircle class="eli-tabela__estado-icone eli-tabela__estado-icone--erro" :size="18" :stroke-width="2" />
|
||||
<div>
|
||||
<div class="eli-tabela__erro-titulo">Erro</div>
|
||||
<div class="eli-tabela__erro-mensagem">{{ erro }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Estado vazio: consulta sem registros -->
|
||||
<div v-else class="eli-tabela eli-tabela--vazio">
|
||||
{{ mensagemVazio ?? "Nenhum registro encontrado." }}
|
||||
<Inbox class="eli-tabela__estado-icone" :size="22" :stroke-width="1.5" />
|
||||
<span>{{ mensagemVazio ?? "Nenhum registro encontrado." }}</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { AlertCircle, Inbox, LoaderCircle } from "@lucide/vue"
|
||||
import { defineComponent, type PropType } from "vue"
|
||||
|
||||
export default defineComponent({
|
||||
name: "EliTabelaEstados",
|
||||
components: { AlertCircle, Inbox, LoaderCircle },
|
||||
props: {
|
||||
carregando: {
|
||||
type: Boolean,
|
||||
|
|
|
|||
|
|
@ -70,7 +70,7 @@ export default defineComponent({
|
|||
props: {
|
||||
colunas: {
|
||||
// biome-ignore lint/suspicious/noExplicitAny: dynamic column
|
||||
type: Array as PropType<Array<tipoEliColuna<any>>>,
|
||||
type: Array as PropType<tipoEliColuna<any>[]>,
|
||||
required: true,
|
||||
},
|
||||
temAcoes: {
|
||||
|
|
@ -109,9 +109,8 @@ export default defineComponent({
|
|||
"alternar-selecao-todos": () => true,
|
||||
},
|
||||
setup(_props, { emit }) {
|
||||
// biome-ignore lint/suspicious/noExplicitAny: dynamic column
|
||||
function isOrdenavel(coluna: any) {
|
||||
return coluna?.coluna_ordem !== undefined && coluna?.coluna_ordem !== null
|
||||
function isOrdenavel(coluna: tipoEliColuna<unknown>): boolean {
|
||||
return coluna.coluna_ordem !== undefined && coluna.coluna_ordem !== null
|
||||
}
|
||||
|
||||
function emitAlternarOrdenacao(chave: string) {
|
||||
|
|
|
|||
|
|
@ -67,7 +67,7 @@ export default defineComponent({
|
|||
},
|
||||
acoes: {
|
||||
// biome-ignore lint/suspicious/noExplicitAny: dynamic typing needed
|
||||
type: Array as PropType<Array<ItemAcao<any>>>,
|
||||
type: Array as PropType<ItemAcao<any>[]>,
|
||||
required: true,
|
||||
},
|
||||
linha: {
|
||||
|
|
|
|||
342
src/componentes/EliTabela/EliTabelaModalColunas.css
Normal file
342
src/componentes/EliTabela/EliTabelaModalColunas.css
Normal file
|
|
@ -0,0 +1,342 @@
|
|||
.eli-tabela-modal-colunas__overlay {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
background: rgba(15, 23, 42, 0.45);
|
||||
backdrop-filter: blur(2px);
|
||||
z-index: 4000;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.eli-tabela-modal-colunas__modal {
|
||||
width: min(820px, 100%);
|
||||
background: var(--cor-eli-superficie);
|
||||
border-radius: 16px;
|
||||
border: 1px solid var(--cor-eli-borda);
|
||||
box-shadow: 0 24px 64px rgba(15, 23, 42, 0.22);
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
/* ── Header ── */
|
||||
.eli-tabela-modal-colunas__header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 14px 16px;
|
||||
border-bottom: 1px solid var(--cor-eli-borda);
|
||||
background: var(--cor-eli-primaria-header, #ADCC44);
|
||||
}
|
||||
|
||||
.eli-tabela-modal-colunas__header-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.eli-tabela-modal-colunas__header-icone {
|
||||
color: var(--cor-eli-primaria-escura);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.eli-tabela-modal-colunas__titulo {
|
||||
font-size: 0.95rem;
|
||||
font-weight: 600;
|
||||
color: var(--cor-eli-primaria-texto);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.eli-tabela-modal-colunas__fechar {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
border-radius: 8px;
|
||||
border: none;
|
||||
background: transparent;
|
||||
cursor: pointer;
|
||||
color: var(--cor-eli-primaria-escura);
|
||||
transition: background 0.2s ease;
|
||||
}
|
||||
|
||||
.eli-tabela-modal-colunas__fechar:hover,
|
||||
.eli-tabela-modal-colunas__fechar:focus-visible {
|
||||
background: rgba(15, 23, 42, 0.1);
|
||||
}
|
||||
|
||||
.eli-tabela-modal-colunas__fechar:focus-visible {
|
||||
outline: 2px solid var(--cor-eli-primaria);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
/* ── Instrução ── */
|
||||
.eli-tabela-modal-colunas__instrucao {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 8px 16px;
|
||||
background: var(--cor-eli-fundo);
|
||||
border-bottom: 1px solid var(--cor-eli-borda);
|
||||
font-size: 0.78rem;
|
||||
color: var(--cor-eli-neutro);
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.eli-tabela-modal-colunas__instrucao-icone {
|
||||
flex-shrink: 0;
|
||||
color: var(--cor-eli-borda-forte);
|
||||
}
|
||||
|
||||
/* ── Conteúdo ── */
|
||||
.eli-tabela-modal-colunas__conteudo {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 0;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.eli-tabela-modal-colunas__coluna {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
.eli-tabela-modal-colunas__coluna + .eli-tabela-modal-colunas__coluna {
|
||||
border-left: 1px solid var(--cor-eli-borda);
|
||||
}
|
||||
|
||||
/* ── Cabeçalho das colunas ── */
|
||||
.eli-tabela-modal-colunas__coluna-cabecalho {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.06em;
|
||||
text-transform: uppercase;
|
||||
padding: 6px 10px;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.eli-tabela-modal-colunas__coluna-cabecalho--visiveis {
|
||||
color: var(--cor-eli-primaria-escura);
|
||||
background: var(--cor-eli-primaria-bg);
|
||||
}
|
||||
|
||||
.eli-tabela-modal-colunas__coluna-cabecalho--invisiveis {
|
||||
color: var(--cor-eli-neutro-escuro);
|
||||
background: var(--cor-eli-fundo);
|
||||
}
|
||||
|
||||
.eli-tabela-modal-colunas__coluna-contador {
|
||||
margin-left: auto;
|
||||
font-size: 0.72rem;
|
||||
font-weight: 700;
|
||||
padding: 1px 7px;
|
||||
border-radius: 9999px;
|
||||
background: rgba(15, 23, 42, 0.08);
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
/* ── Lista ── */
|
||||
.eli-tabela-modal-colunas__lista {
|
||||
flex: 1;
|
||||
min-height: 220px;
|
||||
border: 1.5px dashed var(--cor-eli-borda);
|
||||
border-radius: 10px;
|
||||
padding: 8px;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
transition: border-color 0.2s ease, background 0.2s ease;
|
||||
}
|
||||
|
||||
.eli-tabela-modal-colunas__lista--visiveis {
|
||||
background: rgba(104, 140, 0, 0.02);
|
||||
}
|
||||
|
||||
.eli-tabela-modal-colunas__lista--invisiveis {
|
||||
background: var(--cor-eli-fundo);
|
||||
}
|
||||
|
||||
/* ── Item ── */
|
||||
.eli-tabela-modal-colunas__item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 9px 10px;
|
||||
border-radius: 8px;
|
||||
border: 1px solid var(--cor-eli-borda);
|
||||
background: var(--cor-eli-superficie);
|
||||
cursor: grab;
|
||||
user-select: none;
|
||||
transition: box-shadow 0.15s ease, opacity 0.15s ease, border-color 0.15s ease;
|
||||
}
|
||||
|
||||
.eli-tabela-modal-colunas__item + .eli-tabela-modal-colunas__item {
|
||||
margin-top: 6px;
|
||||
}
|
||||
|
||||
.eli-tabela-modal-colunas__item:hover {
|
||||
border-color: var(--cor-eli-borda-forte);
|
||||
box-shadow: 0 2px 8px rgba(15, 23, 42, 0.08);
|
||||
}
|
||||
|
||||
.eli-tabela-modal-colunas__item:active {
|
||||
cursor: grabbing;
|
||||
}
|
||||
|
||||
.eli-tabela-modal-colunas__item--invisivel {
|
||||
opacity: 0.72;
|
||||
}
|
||||
|
||||
.eli-tabela-modal-colunas__item--arrastando {
|
||||
opacity: 0.15;
|
||||
border-style: dashed;
|
||||
border-color: var(--cor-eli-primaria-media);
|
||||
}
|
||||
|
||||
/* ── Partes do item ── */
|
||||
.eli-tabela-modal-colunas__item-handle {
|
||||
color: var(--cor-eli-borda-forte);
|
||||
flex-shrink: 0;
|
||||
transition: color 0.15s ease;
|
||||
}
|
||||
|
||||
.eli-tabela-modal-colunas__item:hover .eli-tabela-modal-colunas__item-handle {
|
||||
color: var(--cor-eli-neutro);
|
||||
}
|
||||
|
||||
.eli-tabela-modal-colunas__item-texto {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
font-size: 0.875rem;
|
||||
color: var(--cor-eli-neutro-escuro);
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.eli-tabela-modal-colunas__item-badge-icone {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.eli-tabela-modal-colunas__item-badge-icone--visivel {
|
||||
color: var(--cor-eli-primaria-acao);
|
||||
}
|
||||
|
||||
.eli-tabela-modal-colunas__item-badge-icone--invisivel {
|
||||
color: var(--cor-eli-borda-forte);
|
||||
}
|
||||
|
||||
/* ── Estado vazio ── */
|
||||
.eli-tabela-modal-colunas__vazio {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
padding: 24px 12px;
|
||||
text-align: center;
|
||||
font-size: 0.8rem;
|
||||
color: var(--cor-eli-neutro);
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.eli-tabela-modal-colunas__vazio-icone {
|
||||
color: var(--cor-eli-borda-forte);
|
||||
}
|
||||
|
||||
.eli-tabela-modal-colunas__vazio-icone--ok {
|
||||
color: var(--cor-eli-primaria-acao);
|
||||
}
|
||||
|
||||
/* ── Animações TransitionGroup ── */
|
||||
.list-move,
|
||||
.list-enter-active,
|
||||
.list-leave-active {
|
||||
transition: all 0.25s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
|
||||
.list-enter-from,
|
||||
.list-leave-to {
|
||||
opacity: 0;
|
||||
transform: scale(0.95);
|
||||
}
|
||||
|
||||
.list-leave-active {
|
||||
position: absolute;
|
||||
width: calc(100% - 16px);
|
||||
}
|
||||
|
||||
/* ── Footer ── */
|
||||
.eli-tabela-modal-colunas__footer {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 8px;
|
||||
padding: 12px 16px;
|
||||
border-top: 1px solid var(--cor-eli-borda);
|
||||
background: var(--cor-eli-fundo);
|
||||
}
|
||||
|
||||
.eli-tabela-modal-colunas__botao {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
height: 34px;
|
||||
padding: 0 16px;
|
||||
border-radius: 8px;
|
||||
border: 1px solid var(--cor-eli-borda);
|
||||
background: var(--cor-eli-superficie);
|
||||
cursor: pointer;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
transition: background 0.2s ease, border-color 0.2s ease, color 0.2s ease;
|
||||
}
|
||||
|
||||
.eli-tabela-modal-colunas__botao--sec {
|
||||
color: var(--cor-eli-neutro-escuro);
|
||||
}
|
||||
|
||||
.eli-tabela-modal-colunas__botao--sec:hover,
|
||||
.eli-tabela-modal-colunas__botao--sec:focus-visible {
|
||||
background: var(--cor-eli-fundo);
|
||||
border-color: var(--cor-eli-borda-forte);
|
||||
}
|
||||
|
||||
.eli-tabela-modal-colunas__botao--sec:focus-visible {
|
||||
outline: 2px solid var(--cor-eli-primaria-clara);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
.eli-tabela-modal-colunas__botao--prim {
|
||||
border: none;
|
||||
background: var(--cor-eli-primaria);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.eli-tabela-modal-colunas__botao--prim:hover,
|
||||
.eli-tabela-modal-colunas__botao--prim:focus-visible {
|
||||
background: var(--cor-eli-primaria-escura);
|
||||
}
|
||||
|
||||
.eli-tabela-modal-colunas__botao--prim:focus-visible {
|
||||
outline: 2px solid var(--cor-eli-primaria-clara);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
/* ── Responsivo ── */
|
||||
@media (max-width: 720px) {
|
||||
.eli-tabela-modal-colunas__conteudo {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.eli-tabela-modal-colunas__coluna + .eli-tabela-modal-colunas__coluna {
|
||||
border-left: none;
|
||||
border-top: 1px solid var(--cor-eli-borda);
|
||||
}
|
||||
}
|
||||
|
|
@ -5,78 +5,124 @@
|
|||
class="eli-tabela-modal-colunas__modal"
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
aria-label="Configurar colunas"
|
||||
aria-labelledby="eli-modal-colunas-titulo"
|
||||
>
|
||||
<!-- Header -->
|
||||
<header class="eli-tabela-modal-colunas__header">
|
||||
<h3 class="eli-tabela-modal-colunas__titulo">Colunas</h3>
|
||||
<button type="button" class="eli-tabela-modal-colunas__fechar" aria-label="Fechar" @click="emitFechar">
|
||||
×
|
||||
<div class="eli-tabela-modal-colunas__header-info">
|
||||
<Columns2 :size="18" :stroke-width="2" class="eli-tabela-modal-colunas__header-icone" />
|
||||
<h3 id="eli-modal-colunas-titulo" class="eli-tabela-modal-colunas__titulo">Configurar colunas</h3>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
class="eli-tabela-modal-colunas__fechar"
|
||||
aria-label="Fechar modal de colunas"
|
||||
@click="emitFechar"
|
||||
>
|
||||
<X :size="16" :stroke-width="2.5" />
|
||||
</button>
|
||||
</header>
|
||||
|
||||
<!-- Instrução contextual -->
|
||||
<div class="eli-tabela-modal-colunas__instrucao">
|
||||
<GripVertical :size="14" :stroke-width="2" class="eli-tabela-modal-colunas__instrucao-icone" />
|
||||
Arraste para reordenar ou mover entre as listas
|
||||
</div>
|
||||
|
||||
<!-- Conteúdo: duas colunas -->
|
||||
<div class="eli-tabela-modal-colunas__conteudo">
|
||||
|
||||
<!-- Visíveis -->
|
||||
<div class="eli-tabela-modal-colunas__coluna">
|
||||
<div class="eli-tabela-modal-colunas__coluna-titulo">Visíveis</div>
|
||||
<div class="eli-tabela-modal-colunas__coluna-cabecalho eli-tabela-modal-colunas__coluna-cabecalho--visiveis">
|
||||
<Eye :size="14" :stroke-width="2" />
|
||||
<span>Visíveis</span>
|
||||
<span class="eli-tabela-modal-colunas__coluna-contador">{{ visiveisLocal.length }}</span>
|
||||
</div>
|
||||
<TransitionGroup
|
||||
name="list"
|
||||
tag="div"
|
||||
class="eli-tabela-modal-colunas__lista"
|
||||
class="eli-tabela-modal-colunas__lista eli-tabela-modal-colunas__lista--visiveis"
|
||||
@dragover.prevent="onDragOverLista('visiveis')"
|
||||
@drop="onDragEnd"
|
||||
>
|
||||
<div
|
||||
v-for="rotulo in visiveisLocal"
|
||||
:key="`vis-${rotulo}`"
|
||||
class="eli-tabela-modal-colunas__item"
|
||||
class="eli-tabela-modal-colunas__item eli-tabela-modal-colunas__item--visivel"
|
||||
:class="{
|
||||
'eli-tabela-modal-colunas__item--arrastando': itemSendoArrastado === rotulo
|
||||
}"
|
||||
draggable="true"
|
||||
:title="`Arraste para mover '${rotulo}'`"
|
||||
@dragstart="onDragStart($event, rotulo)"
|
||||
@dragend="onDragEnd"
|
||||
@dragover.stop.prevent="onDragOverItem(rotulo, 'visiveis')"
|
||||
@drop.stop.prevent="onDragEnd"
|
||||
>
|
||||
<span class="eli-tabela-modal-colunas__item-handle" aria-hidden="true">⋮⋮</span>
|
||||
<GripVertical :size="15" :stroke-width="2" class="eli-tabela-modal-colunas__item-handle" aria-hidden="true" />
|
||||
<span class="eli-tabela-modal-colunas__item-texto">{{ rotulo }}</span>
|
||||
<Eye :size="13" :stroke-width="2" class="eli-tabela-modal-colunas__item-badge-icone eli-tabela-modal-colunas__item-badge-icone--visivel" aria-hidden="true" />
|
||||
</div>
|
||||
|
||||
<!-- Estado vazio -->
|
||||
<div v-if="visiveisLocal.length === 0" key="vazio-visiveis" class="eli-tabela-modal-colunas__vazio">
|
||||
<EyeOff :size="28" :stroke-width="1.5" class="eli-tabela-modal-colunas__vazio-icone" />
|
||||
<span>Nenhuma coluna visível.<br>Arraste itens da lista ao lado.</span>
|
||||
</div>
|
||||
</TransitionGroup>
|
||||
</div>
|
||||
|
||||
<!-- Invisíveis -->
|
||||
<div class="eli-tabela-modal-colunas__coluna">
|
||||
<div class="eli-tabela-modal-colunas__coluna-titulo">Invisíveis</div>
|
||||
<div class="eli-tabela-modal-colunas__coluna-cabecalho eli-tabela-modal-colunas__coluna-cabecalho--invisiveis">
|
||||
<EyeOff :size="14" :stroke-width="2" />
|
||||
<span>Ocultas</span>
|
||||
<span class="eli-tabela-modal-colunas__coluna-contador">{{ invisiveisLocal.length }}</span>
|
||||
</div>
|
||||
<TransitionGroup
|
||||
name="list"
|
||||
tag="div"
|
||||
class="eli-tabela-modal-colunas__lista"
|
||||
class="eli-tabela-modal-colunas__lista eli-tabela-modal-colunas__lista--invisiveis"
|
||||
@dragover.prevent="onDragOverLista('invisiveis')"
|
||||
@drop="onDragEnd"
|
||||
>
|
||||
<div
|
||||
v-for="rotulo in invisiveisLocal"
|
||||
:key="`inv-${rotulo}`"
|
||||
class="eli-tabela-modal-colunas__item"
|
||||
class="eli-tabela-modal-colunas__item eli-tabela-modal-colunas__item--invisivel"
|
||||
:class="{
|
||||
'eli-tabela-modal-colunas__item--arrastando': itemSendoArrastado === rotulo
|
||||
}"
|
||||
draggable="true"
|
||||
:title="`Arraste para mover '${rotulo}'`"
|
||||
@dragstart="onDragStart($event, rotulo)"
|
||||
@dragend="onDragEnd"
|
||||
@dragover.stop.prevent="onDragOverItem(rotulo, 'invisiveis')"
|
||||
@drop.stop.prevent="onDragEnd"
|
||||
>
|
||||
<span class="eli-tabela-modal-colunas__item-handle" aria-hidden="true">⋮⋮</span>
|
||||
<GripVertical :size="15" :stroke-width="2" class="eli-tabela-modal-colunas__item-handle" aria-hidden="true" />
|
||||
<span class="eli-tabela-modal-colunas__item-texto">{{ rotulo }}</span>
|
||||
<EyeOff :size="13" :stroke-width="2" class="eli-tabela-modal-colunas__item-badge-icone eli-tabela-modal-colunas__item-badge-icone--invisivel" aria-hidden="true" />
|
||||
</div>
|
||||
|
||||
<!-- Estado vazio -->
|
||||
<div v-if="invisiveisLocal.length === 0" key="vazio-invisiveis" class="eli-tabela-modal-colunas__vazio">
|
||||
<Eye :size="28" :stroke-width="1.5" class="eli-tabela-modal-colunas__vazio-icone eli-tabela-modal-colunas__vazio-icone--ok" />
|
||||
<span>Todas as colunas estão visíveis.</span>
|
||||
</div>
|
||||
</TransitionGroup>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Footer -->
|
||||
<footer class="eli-tabela-modal-colunas__footer">
|
||||
<button type="button" class="eli-tabela-modal-colunas__botao eli-tabela-modal-colunas__botao--sec" @click="emitFechar">
|
||||
<X :size="14" :stroke-width="2.5" />
|
||||
Cancelar
|
||||
</button>
|
||||
<button type="button" class="eli-tabela-modal-colunas__botao eli-tabela-modal-colunas__botao--prim" @click="emitSalvar">
|
||||
<Check :size="14" :stroke-width="2.5" />
|
||||
Salvar
|
||||
</button>
|
||||
</footer>
|
||||
|
|
@ -86,30 +132,23 @@
|
|||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, type PropType, ref, watch } from "vue"
|
||||
import { Check, Columns2, Eye, EyeOff, GripVertical, X } from "@lucide/vue"
|
||||
import { computed, defineComponent, type PropType } from "vue"
|
||||
import "./EliTabelaModalColunas.css"
|
||||
import type { EliTabelaColunasConfig } from "./colunasStorage"
|
||||
import type { tipoEliColuna } from "./types-eli-tabela"
|
||||
|
||||
type OrigemLista = "visiveis" | "invisiveis"
|
||||
import { useColunasOrdenacao } from "./useColunasOrdenacao"
|
||||
|
||||
export default defineComponent({
|
||||
name: "EliTabelaModalColunas",
|
||||
components: { Columns2, Eye, EyeOff, GripVertical, X, Check },
|
||||
props: {
|
||||
aberto: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
},
|
||||
rotulosColunas: {
|
||||
type: Array as PropType<string[]>,
|
||||
required: true,
|
||||
},
|
||||
configInicial: {
|
||||
type: Object as PropType<EliTabelaColunasConfig>,
|
||||
required: true,
|
||||
},
|
||||
aberto: { type: Boolean, required: true },
|
||||
rotulosColunas: { type: Array as PropType<string[]>, required: true },
|
||||
configInicial: { type: Object as PropType<EliTabelaColunasConfig>, required: true },
|
||||
colunas: {
|
||||
// biome-ignore lint/suspicious/noExplicitAny: dynamic typing needed
|
||||
type: Array as PropType<Array<tipoEliColuna<any>>>,
|
||||
type: Array as PropType<tipoEliColuna<any>[]>,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
|
|
@ -122,58 +161,12 @@ export default defineComponent({
|
|||
},
|
||||
},
|
||||
setup(props, { emit }) {
|
||||
const visiveisLocal = ref<string[]>([])
|
||||
const invisiveisLocal = ref<string[]>([])
|
||||
|
||||
const itemSendoArrastado = ref<string | null>(null)
|
||||
|
||||
function sincronizarEstado() {
|
||||
const todos = props.rotulosColunas
|
||||
const configTemDados =
|
||||
(props.configInicial.visiveis?.length ?? 0) > 0 ||
|
||||
(props.configInicial.invisiveis?.length ?? 0) > 0
|
||||
|
||||
const invisiveisPadraoSet = new Set(
|
||||
props.colunas.filter((c) => c.visivel === false).map((c) => c.rotulo),
|
||||
)
|
||||
|
||||
const invisiveisSet = configTemDados
|
||||
? new Set(props.configInicial.invisiveis ?? [])
|
||||
: invisiveisPadraoSet
|
||||
|
||||
const baseVisiveis = todos.filter((r) => !invisiveisSet.has(r))
|
||||
|
||||
// ordenação: aplica ordem salva (visiveis) e adiciona novas ao final
|
||||
const ordemSalva = props.configInicial.visiveis ?? []
|
||||
const setVis = new Set(baseVisiveis)
|
||||
const ordenadas: string[] = []
|
||||
|
||||
for (const r of ordemSalva) {
|
||||
if (setVis.has(r)) ordenadas.push(r)
|
||||
}
|
||||
for (const r of baseVisiveis) {
|
||||
if (!ordenadas.includes(r)) ordenadas.push(r)
|
||||
}
|
||||
|
||||
visiveisLocal.value = ordenadas
|
||||
|
||||
// invisíveis: somente as que existem na tabela
|
||||
invisiveisLocal.value = todos.filter((r) => invisiveisSet.has(r))
|
||||
}
|
||||
|
||||
watch(
|
||||
() =>
|
||||
[
|
||||
props.aberto,
|
||||
props.rotulosColunas,
|
||||
props.configInicial,
|
||||
props.colunas,
|
||||
] as const,
|
||||
() => {
|
||||
if (props.aberto) sincronizarEstado()
|
||||
},
|
||||
{ deep: true, immediate: true },
|
||||
)
|
||||
const ordenacao = useColunasOrdenacao({
|
||||
aberto: computed(() => props.aberto),
|
||||
rotulosColunas: computed(() => props.rotulosColunas),
|
||||
configInicial: computed(() => props.configInicial),
|
||||
colunas: computed(() => props.colunas),
|
||||
})
|
||||
|
||||
function emitFechar() {
|
||||
emit("fechar")
|
||||
|
|
@ -181,242 +174,17 @@ export default defineComponent({
|
|||
|
||||
function emitSalvar() {
|
||||
emit("salvar", {
|
||||
visiveis: [...visiveisLocal.value],
|
||||
invisiveis: [...invisiveisLocal.value],
|
||||
visiveis: [...ordenacao.visiveisLocal.value],
|
||||
invisiveis: [...ordenacao.invisiveisLocal.value],
|
||||
})
|
||||
}
|
||||
|
||||
function onDragStart(e: DragEvent, rotulo: string) {
|
||||
itemSendoArrastado.value = rotulo
|
||||
if (e.dataTransfer) {
|
||||
e.dataTransfer.setData("text/plain", rotulo)
|
||||
e.dataTransfer.effectAllowed = "move"
|
||||
}
|
||||
}
|
||||
|
||||
function onDragEnd() {
|
||||
itemSendoArrastado.value = null
|
||||
}
|
||||
|
||||
function onDragOverItem(alvoRotulo: string, destino: OrigemLista) {
|
||||
const draggedRotulo = itemSendoArrastado.value
|
||||
if (!draggedRotulo || draggedRotulo === alvoRotulo) return
|
||||
|
||||
const origemVisIdx = visiveisLocal.value.indexOf(draggedRotulo)
|
||||
const origemInvIdx = invisiveisLocal.value.indexOf(draggedRotulo)
|
||||
const origem =
|
||||
origemVisIdx >= 0 ? "visiveis" : origemInvIdx >= 0 ? "invisiveis" : null
|
||||
if (!origem) return
|
||||
|
||||
const origemArr =
|
||||
origem === "visiveis" ? visiveisLocal.value : invisiveisLocal.value
|
||||
const destinoArr =
|
||||
destino === "visiveis" ? visiveisLocal.value : invisiveisLocal.value
|
||||
|
||||
const dragIdx = origemArr.indexOf(draggedRotulo)
|
||||
const alvoIdx = destinoArr.indexOf(alvoRotulo)
|
||||
|
||||
origemArr.splice(dragIdx, 1)
|
||||
destinoArr.splice(alvoIdx, 0, draggedRotulo)
|
||||
}
|
||||
|
||||
function onDragOverLista(destino: OrigemLista) {
|
||||
const draggedRotulo = itemSendoArrastado.value
|
||||
if (!draggedRotulo) return
|
||||
|
||||
const destinoArr =
|
||||
destino === "visiveis" ? visiveisLocal.value : invisiveisLocal.value
|
||||
if (destinoArr[destinoArr.length - 1] === draggedRotulo) return
|
||||
|
||||
const origemVisIdx = visiveisLocal.value.indexOf(draggedRotulo)
|
||||
const origemInvIdx = invisiveisLocal.value.indexOf(draggedRotulo)
|
||||
if (origemVisIdx >= 0) visiveisLocal.value.splice(origemVisIdx, 1)
|
||||
if (origemInvIdx >= 0) invisiveisLocal.value.splice(origemInvIdx, 1)
|
||||
|
||||
destinoArr.push(draggedRotulo)
|
||||
}
|
||||
|
||||
return {
|
||||
visiveisLocal,
|
||||
invisiveisLocal,
|
||||
itemSendoArrastado,
|
||||
...ordenacao,
|
||||
emitFechar,
|
||||
emitSalvar,
|
||||
onDragStart,
|
||||
onDragEnd,
|
||||
onDragOverItem,
|
||||
onDragOverLista,
|
||||
}
|
||||
},
|
||||
})
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.eli-tabela-modal-colunas__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-colunas__modal {
|
||||
width: min(860px, 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-colunas__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-colunas__titulo {
|
||||
font-size: 1rem;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.eli-tabela-modal-colunas__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-colunas__fechar:hover,
|
||||
.eli-tabela-modal-colunas__fechar:focus-visible {
|
||||
background: rgba(15, 23, 42, 0.06);
|
||||
}
|
||||
|
||||
.eli-tabela-modal-colunas__conteudo {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 12px;
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.eli-tabela-modal-colunas__coluna-titulo {
|
||||
font-weight: 600;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.eli-tabela-modal-colunas__lista {
|
||||
min-height: 260px;
|
||||
border: 1px solid rgba(15, 23, 42, 0.12);
|
||||
border-radius: 12px;
|
||||
padding: 10px;
|
||||
background: rgba(15, 23, 42, 0.01);
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.eli-tabela-modal-colunas__item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 10px 10px;
|
||||
border-radius: 10px;
|
||||
border: 1px solid rgba(15, 23, 42, 0.08);
|
||||
background: #fff;
|
||||
cursor: grab;
|
||||
user-select: none;
|
||||
transition: box-shadow 0.15s, opacity 0.15s, transform 0.15s;
|
||||
}
|
||||
|
||||
.eli-tabela-modal-colunas__item + .eli-tabela-modal-colunas__item {
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.eli-tabela-modal-colunas__item:active {
|
||||
cursor: grabbing;
|
||||
}
|
||||
|
||||
.eli-tabela-modal-colunas__item--arrastando {
|
||||
opacity: 0.1;
|
||||
background: #f8fafc;
|
||||
border-style: dashed;
|
||||
}
|
||||
|
||||
/* Animações de reposicionamento e listas do Vue TransitionGroup */
|
||||
.list-move,
|
||||
.list-enter-active,
|
||||
.list-leave-active {
|
||||
transition: all 0.25s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
|
||||
.list-enter-from,
|
||||
.list-leave-to {
|
||||
opacity: 0;
|
||||
transform: scale(0.95);
|
||||
}
|
||||
|
||||
.list-leave-active {
|
||||
/* position absolute para suavizar a animação de saída de outras listas */
|
||||
position: absolute;
|
||||
width: calc(100% - 20px);
|
||||
}
|
||||
|
||||
.eli-tabela-modal-colunas__item-handle {
|
||||
color: rgba(15, 23, 42, 0.55);
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.eli-tabela-modal-colunas__item-texto {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.eli-tabela-modal-colunas__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-colunas__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-colunas__botao--sec:hover,
|
||||
.eli-tabela-modal-colunas__botao--sec:focus-visible {
|
||||
background: rgba(15, 23, 42, 0.06);
|
||||
}
|
||||
|
||||
.eli-tabela-modal-colunas__botao--prim {
|
||||
border: none;
|
||||
background: rgba(37, 99, 235, 0.95);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.eli-tabela-modal-colunas__botao--prim:hover,
|
||||
.eli-tabela-modal-colunas__botao--prim:focus-visible {
|
||||
background: rgba(37, 99, 235, 1);
|
||||
}
|
||||
|
||||
@media (max-width: 720px) {
|
||||
.eli-tabela-modal-colunas__conteudo {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
443
src/componentes/EliTabela/EliTabelaModalFiltroAvancado.css
Normal file
443
src/componentes/EliTabela/EliTabelaModalFiltroAvancado.css
Normal file
|
|
@ -0,0 +1,443 @@
|
|||
.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;
|
||||
}
|
||||
|
||||
/* ── Header ──────────────────────────────────── */
|
||||
.eli-tabela-modal-filtro__header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 14px 20px;
|
||||
border-bottom: 1px solid rgba(15, 23, 42, 0.08);
|
||||
}
|
||||
|
||||
.eli-tabela-modal-filtro__header-titulo {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.eli-tabela-modal-filtro__titulo {
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
margin: 0;
|
||||
color: rgba(15, 23, 42, 0.9);
|
||||
}
|
||||
|
||||
.eli-tabela-modal-filtro__header-icone {
|
||||
color: var(--cor-eli-primaria);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.eli-tabela-modal-filtro__contador {
|
||||
font-size: 0.75rem;
|
||||
font-weight: 500;
|
||||
color: var(--cor-eli-primaria);
|
||||
background: var(--cor-eli-primaria-bg);
|
||||
padding: 2px 8px;
|
||||
border-radius: 10px;
|
||||
letter-spacing: 0.01em;
|
||||
}
|
||||
|
||||
.eli-tabela-modal-filtro__fechar {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 34px;
|
||||
height: 34px;
|
||||
border-radius: 10px;
|
||||
border: none;
|
||||
background: transparent;
|
||||
cursor: pointer;
|
||||
color: rgba(15, 23, 42, 0.55);
|
||||
transition: background 0.15s, color 0.15s;
|
||||
}
|
||||
|
||||
.eli-tabela-modal-filtro__fechar:hover,
|
||||
.eli-tabela-modal-filtro__fechar:focus-visible {
|
||||
background: rgba(15, 23, 42, 0.06);
|
||||
color: rgba(15, 23, 42, 0.9);
|
||||
}
|
||||
|
||||
/* ── Conteúdo ────────────────────────────────── */
|
||||
.eli-tabela-modal-filtro__conteudo {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.eli-tabela-modal-filtro__vazio {
|
||||
opacity: 0.65;
|
||||
font-size: 0.85rem;
|
||||
padding: 12px 0 4px;
|
||||
}
|
||||
|
||||
/* ── Estado vazio orientado ──────────────────── */
|
||||
.eli-tabela-modal-filtro__estado-vazio {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 28px 16px;
|
||||
color: rgba(15, 23, 42, 0.35);
|
||||
border: 1px dashed rgba(15, 23, 42, 0.12);
|
||||
border-radius: 10px;
|
||||
background: rgba(15, 23, 42, 0.015);
|
||||
}
|
||||
|
||||
.eli-tabela-modal-filtro__estado-vazio span {
|
||||
font-size: 0.85rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* ── Lista de filtros (2 colunas) ────────────── */
|
||||
.eli-tabela-modal-filtro__lista {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.eli-tabela-modal-filtro__linha {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
padding: 10px 12px;
|
||||
border: 1px solid rgba(15, 23, 42, 0.08);
|
||||
border-radius: 10px;
|
||||
background: rgba(15, 23, 42, 0.015);
|
||||
transition: border-color 0.15s;
|
||||
}
|
||||
|
||||
.eli-tabela-modal-filtro__linha:hover {
|
||||
border-color: var(--cor-eli-primaria-clara);
|
||||
}
|
||||
|
||||
.eli-tabela-modal-filtro__linha-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.eli-tabela-modal-filtro__rotulo-filtro {
|
||||
font-size: 0.82rem;
|
||||
font-weight: 500;
|
||||
color: rgba(15, 23, 42, 0.65);
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.eli-tabela-modal-filtro__id-badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 32px;
|
||||
height: 24px;
|
||||
border-radius: 6px;
|
||||
background: var(--cor-eli-primaria-bg);
|
||||
color: var(--cor-eli-primaria);
|
||||
font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, monospace;
|
||||
font-size: 0.72rem;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.01em;
|
||||
cursor: default;
|
||||
user-select: none;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.eli-tabela-modal-filtro__entrada {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.eli-tabela-modal-filtro__remover {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 26px;
|
||||
height: 26px;
|
||||
border-radius: 8px;
|
||||
border: none;
|
||||
background: transparent;
|
||||
cursor: pointer;
|
||||
color: rgba(15, 23, 42, 0.35);
|
||||
transition: background 0.15s, color 0.15s;
|
||||
margin-left: auto;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.eli-tabela-modal-filtro__remover:hover,
|
||||
.eli-tabela-modal-filtro__remover:focus-visible {
|
||||
background: rgba(239, 68, 68, 0.08);
|
||||
color: rgba(220, 38, 38, 0.9);
|
||||
border-color: rgba(239, 68, 68, 0.2);
|
||||
}
|
||||
|
||||
/* ── Rodapé: select + fórmula lado a lado ────── */
|
||||
.eli-tabela-modal-filtro__rodape-filtros {
|
||||
margin-top: 14px;
|
||||
padding-top: 14px;
|
||||
border-top: 1px solid rgba(15, 23, 42, 0.07);
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
align-items: flex-end;
|
||||
}
|
||||
|
||||
.eli-tabela-modal-filtro__acoes {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.eli-tabela-modal-filtro__select {
|
||||
height: 36px;
|
||||
border-radius: 10px;
|
||||
border: 1px solid rgba(15, 23, 42, 0.12);
|
||||
padding: 0 10px;
|
||||
background: #fff;
|
||||
font-size: 0.875rem;
|
||||
color: rgba(15, 23, 42, 0.85);
|
||||
transition: border-color 0.15s;
|
||||
}
|
||||
|
||||
.eli-tabela-modal-filtro__select:focus {
|
||||
outline: none;
|
||||
border-color: var(--cor-eli-primaria);
|
||||
}
|
||||
|
||||
/* ── Fórmula de combinação ───────────────────── */
|
||||
.eli-tabela-modal-filtro__formula-container {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.eli-tabela-modal-filtro__formula-label {
|
||||
font-size: 0.82rem;
|
||||
font-weight: 500;
|
||||
color: rgba(15, 23, 42, 0.55);
|
||||
white-space: nowrap;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
|
||||
|
||||
.eli-tabela-modal-filtro__formula-ajuda {
|
||||
position: relative;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
color: rgba(15, 23, 42, 0.35);
|
||||
cursor: help;
|
||||
transition: color 0.15s;
|
||||
}
|
||||
|
||||
.eli-tabela-modal-filtro__formula-ajuda:hover,
|
||||
.eli-tabela-modal-filtro__formula-ajuda:focus-visible {
|
||||
color: var(--cor-eli-primaria);
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.eli-tabela-modal-filtro__formula-ajuda:hover .eli-tabela-modal-filtro__formula-tooltip,
|
||||
.eli-tabela-modal-filtro__formula-ajuda:focus-visible .eli-tabela-modal-filtro__formula-tooltip {
|
||||
opacity: 1;
|
||||
pointer-events: auto;
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
.eli-tabela-modal-filtro__formula-tooltip {
|
||||
position: absolute;
|
||||
bottom: calc(100% + 10px);
|
||||
right: 0;
|
||||
width: 280px;
|
||||
background: #0f172a;
|
||||
color: #e2e8f0;
|
||||
border-radius: 10px;
|
||||
padding: 14px 16px;
|
||||
font-size: 0.8rem;
|
||||
line-height: 1.55;
|
||||
z-index: 100;
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
transform: translateY(4px);
|
||||
transition: opacity 0.18s ease, transform 0.18s ease;
|
||||
box-shadow: 0 8px 30px rgba(0, 0, 0, 0.35);
|
||||
}
|
||||
|
||||
.eli-tabela-modal-filtro__formula-tooltip::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
right: 6px;
|
||||
border: 6px solid transparent;
|
||||
border-top-color: #0f172a;
|
||||
}
|
||||
|
||||
.eli-tabela-modal-filtro__formula-tooltip strong {
|
||||
font-size: 0.82rem;
|
||||
color: var(--cor-eli-fundo);
|
||||
display: block;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.eli-tabela-modal-filtro__formula-tooltip p {
|
||||
margin: 6px 0 4px;
|
||||
color: #94a3b8;
|
||||
}
|
||||
|
||||
.eli-tabela-modal-filtro__formula-tooltip ul {
|
||||
margin: 4px 0 8px 14px;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.eli-tabela-modal-filtro__formula-tooltip li {
|
||||
margin-bottom: 3px;
|
||||
color: #cbd5e1;
|
||||
}
|
||||
|
||||
.eli-tabela-modal-filtro__formula-tooltip code {
|
||||
font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, monospace;
|
||||
color: #7dd3fc;
|
||||
font-size: 0.78rem;
|
||||
background: rgba(255, 255, 255, 0.07);
|
||||
border-radius: 4px;
|
||||
padding: 1px 4px;
|
||||
}
|
||||
|
||||
.eli-tabela-modal-filtro__formula-input {
|
||||
width: 220px;
|
||||
height: 36px;
|
||||
border-radius: 10px;
|
||||
border: 1px solid rgba(15, 23, 42, 0.12);
|
||||
padding: 0 10px;
|
||||
font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, monospace;
|
||||
font-size: 0.85rem;
|
||||
background: var(--cor-eli-fundo);
|
||||
transition: border-color 0.15s, background 0.15s;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.eli-tabela-modal-filtro__formula-input:focus {
|
||||
outline: none;
|
||||
border-color: var(--cor-eli-primaria);
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.eli-tabela-modal-filtro__formula-tooltip-nota {
|
||||
margin-top: 8px;
|
||||
opacity: 0.75;
|
||||
}
|
||||
|
||||
/* ── Footer ──────────────────────────────────── */
|
||||
.eli-tabela-modal-filtro__footer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 14px 20px;
|
||||
border-top: 1px solid rgba(15, 23, 42, 0.08);
|
||||
}
|
||||
|
||||
.eli-tabela-modal-filtro__footer-spacer {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
/* ── Botões ──────────────────────────────────── */
|
||||
.eli-tabela-modal-filtro__botao {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
height: 36px;
|
||||
padding: 0 16px;
|
||||
border-radius: 10px;
|
||||
border: 1px solid rgba(15, 23, 42, 0.12);
|
||||
background: #fff;
|
||||
cursor: pointer;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
transition: background 0.15s, color 0.15s, border-color 0.15s;
|
||||
}
|
||||
|
||||
.eli-tabela-modal-filtro__botao:disabled {
|
||||
opacity: 0.45;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.eli-tabela-modal-filtro__botao--sec:hover:not(:disabled),
|
||||
.eli-tabela-modal-filtro__botao--sec:focus-visible {
|
||||
background: rgba(15, 23, 42, 0.05);
|
||||
}
|
||||
|
||||
.eli-tabela-modal-filtro__botao--prim {
|
||||
border: none;
|
||||
background: var(--cor-eli-primaria);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.eli-tabela-modal-filtro__botao--prim:hover:not(:disabled),
|
||||
.eli-tabela-modal-filtro__botao--prim:focus-visible {
|
||||
background: var(--cor-eli-primaria-escura);
|
||||
}
|
||||
|
||||
.eli-tabela-modal-filtro__botao--perigo {
|
||||
color: rgba(220, 38, 38, 0.85);
|
||||
border-color: rgba(220, 38, 38, 0.18);
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.eli-tabela-modal-filtro__botao--perigo:hover:not(:disabled),
|
||||
.eli-tabela-modal-filtro__botao--perigo:focus-visible {
|
||||
background: rgba(239, 68, 68, 0.06);
|
||||
border-color: rgba(220, 38, 38, 0.3);
|
||||
color: rgba(220, 38, 38, 1);
|
||||
}
|
||||
|
||||
/* ── Responsivo ──────────────────────────────── */
|
||||
@media (max-width: 600px) {
|
||||
.eli-tabela-modal-filtro__lista {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
.eli-tabela-modal-filtro__rodape-filtros {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
}
|
||||
.eli-tabela-modal-filtro__acoes {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
}
|
||||
|
||||
/* ── Transição de abertura/fechamento ────────── */
|
||||
.eli-tabela-modal-filtro-enter-active,
|
||||
.eli-tabela-modal-filtro-leave-active {
|
||||
transition: opacity 0.2s ease;
|
||||
}
|
||||
|
||||
.eli-tabela-modal-filtro-enter-active .eli-tabela-modal-filtro__modal,
|
||||
.eli-tabela-modal-filtro-leave-active .eli-tabela-modal-filtro__modal {
|
||||
transition: transform 0.2s ease, opacity 0.2s ease;
|
||||
}
|
||||
|
||||
.eli-tabela-modal-filtro-enter-from,
|
||||
.eli-tabela-modal-filtro-leave-to {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.eli-tabela-modal-filtro-enter-from .eli-tabela-modal-filtro__modal,
|
||||
.eli-tabela-modal-filtro-leave-to .eli-tabela-modal-filtro__modal {
|
||||
transform: scale(0.96);
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
87
src/componentes/EliTabela/EliTabelaPaginacao.css
Normal file
87
src/componentes/EliTabela/EliTabelaPaginacao.css
Normal file
|
|
@ -0,0 +1,87 @@
|
|||
.eli-tabela__paginacao {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
gap: 12px;
|
||||
margin-top: 12px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.eli-tabela__pagina-botao {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 6px;
|
||||
padding: 6px 14px;
|
||||
border-radius: 9999px;
|
||||
border: 1px solid rgba(15, 23, 42, 0.12);
|
||||
background: var(--cor-eli-superficie);
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
color: rgba(15, 23, 42, 0.82);
|
||||
cursor: pointer;
|
||||
transition: background-color 0.2s ease, border-color 0.2s ease,
|
||||
color 0.2s ease;
|
||||
}
|
||||
|
||||
.eli-tabela__pagina-botao:hover,
|
||||
.eli-tabela__pagina-botao:focus-visible {
|
||||
background-color: var(--cor-eli-primaria-bg);
|
||||
border-color: var(--cor-eli-primaria-clara);
|
||||
color: var(--cor-eli-primaria-escura);
|
||||
}
|
||||
|
||||
.eli-tabela__pagina-botao:focus-visible {
|
||||
outline: 2px solid var(--cor-eli-primaria-clara);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
.eli-tabela__pagina-botao:disabled {
|
||||
cursor: default;
|
||||
opacity: 0.5;
|
||||
background: rgba(148, 163, 184, 0.08);
|
||||
border-color: rgba(148, 163, 184, 0.18);
|
||||
color: rgba(71, 85, 105, 0.75);
|
||||
}
|
||||
|
||||
.eli-tabela__pagina-botao--ativo {
|
||||
background: var(--cor-eli-primaria-bg);
|
||||
border-color: var(--cor-eli-primaria);
|
||||
color: var(--cor-eli-primaria-escura);
|
||||
}
|
||||
|
||||
.eli-tabela__pagina-ellipsis {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 32px;
|
||||
color: var(--cor-eli-neutro);
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.eli-tabela__acao-inferior {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
height: 34px;
|
||||
padding: 0 14px;
|
||||
border-radius: 8px;
|
||||
border: 1px solid var(--cor-eli-primaria);
|
||||
background: var(--cor-eli-superficie);
|
||||
color: var(--cor-eli-primaria);
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
line-height: 1;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.2s ease, color 0.2s ease;
|
||||
}
|
||||
|
||||
.eli-tabela__acao-inferior:hover,
|
||||
.eli-tabela__acao-inferior:focus-visible {
|
||||
background-color: var(--cor-eli-primaria-bg);
|
||||
}
|
||||
|
||||
.eli-tabela__acao-inferior:focus-visible {
|
||||
outline: 2px solid var(--cor-eli-primaria);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
|
@ -77,7 +77,7 @@
|
|||
|
||||
<script lang="ts">
|
||||
import { computed, defineComponent, type PropType } from "vue"
|
||||
|
||||
import "./EliTabelaPaginacao.css"
|
||||
export default defineComponent({
|
||||
name: "EliTabelaPaginacao",
|
||||
props: {
|
||||
|
|
@ -209,9 +209,7 @@ export default defineComponent({
|
|||
}
|
||||
|
||||
const anteriorDesabilitado = computed(() => props.pagina <= 1)
|
||||
const proximaDesabilitada = computed(
|
||||
() => props.pagina >= props.totalPaginas,
|
||||
)
|
||||
const proximaDesabilitada = computed(() => props.pagina >= props.totalPaginas)
|
||||
const paginaAtual = computed(() => props.pagina)
|
||||
const totalPaginasExibidas = computed(() => props.totalPaginas)
|
||||
|
||||
|
|
@ -227,92 +225,3 @@ export default defineComponent({
|
|||
})
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.eli-tabela__paginacao {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
gap: 12px;
|
||||
margin-top: 12px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.eli-tabela__pagina-botao {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 6px;
|
||||
padding: 6px 14px;
|
||||
border-radius: 9999px;
|
||||
border: 1px solid rgba(15, 23, 42, 0.12);
|
||||
background: #ffffff;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
color: rgba(15, 23, 42, 0.82);
|
||||
cursor: pointer;
|
||||
transition: background-color 0.2s ease, border-color 0.2s ease,
|
||||
color 0.2s ease;
|
||||
}
|
||||
|
||||
.eli-tabela__pagina-botao:hover,
|
||||
.eli-tabela__pagina-botao:focus-visible {
|
||||
background-color: rgba(37, 99, 235, 0.08);
|
||||
border-color: rgba(37, 99, 235, 0.4);
|
||||
color: rgba(37, 99, 235, 0.95);
|
||||
}
|
||||
|
||||
.eli-tabela__pagina-botao:focus-visible {
|
||||
outline: 2px solid rgba(37, 99, 235, 0.45);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
.eli-tabela__pagina-botao:disabled {
|
||||
cursor: default;
|
||||
opacity: 0.5;
|
||||
background: rgba(148, 163, 184, 0.08);
|
||||
border-color: rgba(148, 163, 184, 0.18);
|
||||
color: rgba(71, 85, 105, 0.75);
|
||||
}
|
||||
|
||||
.eli-tabela__pagina-botao--ativo {
|
||||
background: rgba(37, 99, 235, 0.12);
|
||||
border-color: rgba(37, 99, 235, 0.4);
|
||||
color: rgba(37, 99, 235, 0.95);
|
||||
}
|
||||
|
||||
.eli-tabela__pagina-ellipsis {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 32px;
|
||||
color: rgba(107, 114, 128, 0.85);
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.eli-tabela__acao-inferior {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
height: 34px;
|
||||
padding: 0 14px;
|
||||
border-radius: 8px;
|
||||
border: 1px solid #16a34a; /* default green */
|
||||
background: #ffffff;
|
||||
color: #16a34a;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
line-height: 1;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.2s ease, color 0.2s ease;
|
||||
}
|
||||
|
||||
.eli-tabela__acao-inferior:hover,
|
||||
.eli-tabela__acao-inferior:focus-visible {
|
||||
background-color: #f0fdf4;
|
||||
}
|
||||
|
||||
.eli-tabela__acao-inferior:focus-visible {
|
||||
outline: 2px solid #16a34a;
|
||||
outline-offset: 2px;
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -29,15 +29,11 @@ export default defineComponent({
|
|||
},
|
||||
setup(props) {
|
||||
const tipo = computed(() => props.celula[0] as tipoTabelaCelula)
|
||||
const dados = computed(
|
||||
() => props.celula[1] as tiposTabelaCelulas[tipoTabelaCelula],
|
||||
)
|
||||
const dados = computed(() => props.celula[1] as tiposTabelaCelulas[tipoTabelaCelula])
|
||||
|
||||
// Observação: mantemos o registry tipado, mas o TS do template não consegue
|
||||
// fazer narrowing do componente com base em `tipo`, então tipamos como `Component`.
|
||||
const Componente = computed(
|
||||
() => registryTabelaCelulas[tipo.value] as unknown as Component,
|
||||
)
|
||||
const Componente = computed(() => registryTabelaCelulas[tipo.value] as unknown as Component)
|
||||
|
||||
const dadosParaComponente = computed(() => dados.value)
|
||||
|
||||
|
|
|
|||
|
|
@ -12,7 +12,13 @@
|
|||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { dayjsbr } from "p-comuns"
|
||||
import dayjs from "dayjs"
|
||||
import relativeTime from "dayjs/plugin/relativeTime"
|
||||
import "dayjs/locale/pt-br"
|
||||
|
||||
dayjs.extend(relativeTime)
|
||||
dayjs.locale("pt-br")
|
||||
|
||||
import { computed, defineComponent, type PropType } from "vue"
|
||||
import type { tiposTabelaCelulas } from "./tiposTabelaCelulas"
|
||||
|
||||
|
|
@ -24,27 +30,27 @@ export default defineComponent({
|
|||
required: false,
|
||||
},
|
||||
},
|
||||
setup({ dados }) {
|
||||
setup(props) {
|
||||
const textoData = computed(() => {
|
||||
const valorIso = dados?.valor
|
||||
const valorIso = props.dados?.valor
|
||||
if (!valorIso) return ""
|
||||
|
||||
const formato = dados?.formato ?? "data"
|
||||
const formato = props.dados?.formato ?? "data"
|
||||
|
||||
if (formato === "relativo") {
|
||||
return dayjsbr(valorIso).fromNow()
|
||||
return dayjs(valorIso).fromNow()
|
||||
}
|
||||
|
||||
if (formato === "data_hora") {
|
||||
// Padrão pt-BR simples (sem depender de locale do dayjsbr)
|
||||
return dayjsbr(valorIso).format("DD/MM/YYYY HH:mm")
|
||||
return dayjs(valorIso).format("DD/MM/YYYY HH:mm")
|
||||
}
|
||||
|
||||
// formato === "data"
|
||||
return dayjsbr(valorIso).format("DD/MM/YYYY")
|
||||
return dayjs(valorIso).format("DD/MM/YYYY")
|
||||
})
|
||||
|
||||
return { dados, textoData }
|
||||
return { textoData }
|
||||
},
|
||||
})
|
||||
</script>
|
||||
|
|
@ -56,7 +62,7 @@ export default defineComponent({
|
|||
color: #2563eb;
|
||||
cursor: pointer;
|
||||
text-decoration: underline;
|
||||
text-decoration-color: rgba(37, 99, 235, 0.55);
|
||||
text-decoration-color: var(--cor-eli-primaria-clara);
|
||||
text-underline-offset: 2px;
|
||||
}
|
||||
|
||||
|
|
@ -66,7 +72,7 @@ export default defineComponent({
|
|||
}
|
||||
|
||||
.eli-tabela__celula-link:focus-visible {
|
||||
outline: 2px solid rgba(37, 99, 235, 0.45);
|
||||
outline: 2px solid var(--cor-eli-primaria-clara);
|
||||
outline-offset: 2px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -35,12 +35,12 @@ export default defineComponent({
|
|||
type: Object as PropType<tiposTabelaCelulas["numero"]>,
|
||||
},
|
||||
},
|
||||
setup({ dados }) {
|
||||
setup(props) {
|
||||
const textoNumero = computed(() => {
|
||||
// Mantemos o comportamento anterior (trocar "." por ","), mas agora suportamos prefixo/sufixo.
|
||||
const numero = String(dados?.numero).replace(".", ",")
|
||||
const prefixo = dados?.prefixo?.trim()
|
||||
const sufixo = dados?.sufixo?.trim()
|
||||
const numero = String(props.dados?.numero).replace(".", ",")
|
||||
const prefixo = props.dados?.prefixo?.trim()
|
||||
const sufixo = props.dados?.sufixo?.trim()
|
||||
|
||||
const inicio = prefixo ? `${prefixo} ` : ""
|
||||
const fim = sufixo ? ` ${sufixo}` : ""
|
||||
|
|
@ -48,7 +48,7 @@ export default defineComponent({
|
|||
return `${inicio}${numero}${fim}`
|
||||
})
|
||||
|
||||
return { dados, textoNumero }
|
||||
return { textoNumero }
|
||||
},
|
||||
})
|
||||
</script>
|
||||
|
|
@ -60,7 +60,7 @@ export default defineComponent({
|
|||
color: #2563eb;
|
||||
cursor: pointer;
|
||||
text-decoration: underline;
|
||||
text-decoration-color: rgba(37, 99, 235, 0.55);
|
||||
text-decoration-color: var(--cor-eli-primaria-clara);
|
||||
text-underline-offset: 2px;
|
||||
}
|
||||
|
||||
|
|
@ -70,7 +70,7 @@ export default defineComponent({
|
|||
}
|
||||
|
||||
.eli-tabela__celula-link:focus-visible {
|
||||
outline: 2px solid rgba(37, 99, 235, 0.45);
|
||||
outline: 2px solid var(--cor-eli-primaria-clara);
|
||||
outline-offset: 2px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -37,9 +37,6 @@ export default defineComponent({
|
|||
required: false,
|
||||
},
|
||||
},
|
||||
setup({ dados }) {
|
||||
return { dados }
|
||||
},
|
||||
})
|
||||
</script>
|
||||
|
||||
|
|
|
|||
|
|
@ -26,9 +26,6 @@ export default defineComponent({
|
|||
return {}
|
||||
},
|
||||
methods: {},
|
||||
setup({ dados }) {
|
||||
return { dados }
|
||||
},
|
||||
})
|
||||
</script>
|
||||
|
||||
|
|
@ -39,7 +36,7 @@ export default defineComponent({
|
|||
color: #2563eb;
|
||||
cursor: pointer;
|
||||
text-decoration: underline;
|
||||
text-decoration-color: rgba(37, 99, 235, 0.55);
|
||||
text-decoration-color: var(--cor-eli-primaria-clara);
|
||||
text-underline-offset: 2px;
|
||||
}
|
||||
|
||||
|
|
@ -49,7 +46,7 @@ export default defineComponent({
|
|||
}
|
||||
|
||||
.eli-tabela__celula-link:focus-visible {
|
||||
outline: 2px solid rgba(37, 99, 235, 0.45);
|
||||
outline: 2px solid var(--cor-eli-primaria-clara);
|
||||
outline-offset: 2px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -23,9 +23,6 @@ export default defineComponent({
|
|||
type: Object as PropType<tiposTabelaCelulas["textoTruncado"]>,
|
||||
},
|
||||
},
|
||||
setup({ dados }) {
|
||||
return { dados }
|
||||
},
|
||||
})
|
||||
</script>
|
||||
|
||||
|
|
@ -45,7 +42,7 @@ export default defineComponent({
|
|||
color: #2563eb;
|
||||
cursor: pointer;
|
||||
text-decoration: underline;
|
||||
text-decoration-color: rgba(37, 99, 235, 0.55);
|
||||
text-decoration-color: var(--cor-eli-primaria-clara);
|
||||
text-underline-offset: 2px;
|
||||
}
|
||||
|
||||
|
|
@ -55,7 +52,7 @@ export default defineComponent({
|
|||
}
|
||||
|
||||
.eli-tabela__celula-link:focus-visible {
|
||||
outline: 2px solid rgba(37, 99, 235, 0.45);
|
||||
outline: 2px solid var(--cor-eli-primaria-clara);
|
||||
outline-offset: 2px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,6 +6,16 @@ import EliTabelaCelulaTextoSimples from "./EliTabelaCelulaTextoSimples.vue"
|
|||
import EliTabelaCelulaTextoTruncado from "./EliTabelaCelulaTextoTruncado.vue"
|
||||
import type { tipoTabelaCelula } from "./tiposTabelaCelulas"
|
||||
|
||||
/**
|
||||
* Mapa (dispatch table) que associa cada `tipoTabelaCelula` ao seu componente Vue de renderização.
|
||||
* Usado por `EliTabelaCelula.vue` para resolver dinamicamente qual componente renderizar
|
||||
* a partir do tipo retornado por `tipoEliColuna.celula(linha)`.
|
||||
*
|
||||
* Adicionar um novo tipo de célula requer:
|
||||
* 1. Criar o componente `EliTabelaCelula<Tipo>.vue`
|
||||
* 2. Adicionar a chave em `tipoTabelaCelula` (tiposTabelaCelulas.ts)
|
||||
* 3. Registrar aqui no mapa
|
||||
*/
|
||||
export const registryTabelaCelulas = {
|
||||
textoSimples: EliTabelaCelulaTextoSimples,
|
||||
textoTruncado: EliTabelaCelulaTextoTruncado,
|
||||
|
|
|
|||
|
|
@ -1,12 +1,18 @@
|
|||
export type EliTabelaColunasConfig = {
|
||||
/** Rotulos das colunas visiveis (em ordem). */
|
||||
/** Rótulos das colunas visíveis (em ordem). */
|
||||
visiveis: string[]
|
||||
/** Rotulos das colunas invisiveis. */
|
||||
/** Rótulos das colunas invisíveis. */
|
||||
invisiveis: string[]
|
||||
}
|
||||
|
||||
const STORAGE_PREFIX = "eli:tabela"
|
||||
|
||||
/**
|
||||
* Retorna a chave de `localStorage` utilizada para persistir a configuração de colunas de uma tabela.
|
||||
*
|
||||
* @param nomeTabela - Identificador único da tabela (campo `nome` de `tipoEliTabelaConsulta`)
|
||||
* @returns Chave no formato `eli:tabela:<nomeTabela>:colunas`
|
||||
*/
|
||||
export function storageKeyColunas(nomeTabela: string) {
|
||||
return `${STORAGE_PREFIX}:${nomeTabela}:colunas`
|
||||
}
|
||||
|
|
@ -26,9 +32,14 @@ function normalizarConfig(valor: unknown): EliTabelaColunasConfig {
|
|||
return { visiveis, invisiveis }
|
||||
}
|
||||
|
||||
export function carregarConfigColunas(
|
||||
nomeTabela: string,
|
||||
): EliTabelaColunasConfig {
|
||||
/**
|
||||
* Carrega do `localStorage` a configuração de visibilidade e ordem das colunas de uma tabela.
|
||||
* Em caso de dado corrompido ou ausente, retorna configuração vazia (sem colunas fixadas).
|
||||
*
|
||||
* @param nomeTabela - Identificador único da tabela
|
||||
* @returns Configuração salva ou `{ visiveis: [], invisiveis: [] }` como fallback seguro
|
||||
*/
|
||||
export function carregarConfigColunas(nomeTabela: string): EliTabelaColunasConfig {
|
||||
try {
|
||||
const raw = window.localStorage.getItem(storageKeyColunas(nomeTabela))
|
||||
if (!raw) return { visiveis: [], invisiveis: [] }
|
||||
|
|
@ -38,24 +49,34 @@ export function carregarConfigColunas(
|
|||
}
|
||||
}
|
||||
|
||||
export function salvarConfigColunas(
|
||||
nomeTabela: string,
|
||||
config: EliTabelaColunasConfig,
|
||||
) {
|
||||
/**
|
||||
* Persiste no `localStorage` a configuração de visibilidade e ordem das colunas.
|
||||
* Falhas de armazenamento (modo privado, storage cheio) são silenciadas.
|
||||
*
|
||||
* @param nomeTabela - Identificador único da tabela
|
||||
* @param config - Configuração com listas de colunas visíveis e invisíveis
|
||||
*/
|
||||
export function salvarConfigColunas(nomeTabela: string, config: EliTabelaColunasConfig) {
|
||||
try {
|
||||
window.localStorage.setItem(
|
||||
storageKeyColunas(nomeTabela),
|
||||
JSON.stringify(normalizarConfig(config)),
|
||||
)
|
||||
} catch {
|
||||
// ignore
|
||||
// Silencia erros de storage (modo privado, quota excedida)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove do `localStorage` a configuração de colunas salva para uma tabela.
|
||||
* Usado para restaurar o estado padrão de visibilidade definido nas props.
|
||||
*
|
||||
* @param nomeTabela - Identificador único da tabela
|
||||
*/
|
||||
export function limparConfigColunas(nomeTabela: string) {
|
||||
try {
|
||||
window.localStorage.removeItem(storageKeyColunas(nomeTabela))
|
||||
} catch {
|
||||
// ignore
|
||||
// Silencia erros de storage
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,43 +1,77 @@
|
|||
export type EliTabelaFiltroAvancadoSalvo<T> = Array<{
|
||||
coluna: keyof T
|
||||
|
||||
valor: T[keyof T]
|
||||
}>
|
||||
export type EliTabelaFiltroAvancadoSalvo<T> = {
|
||||
linhas: Array<{
|
||||
chave: keyof T
|
||||
/** Valor vindo de JSON.parse — tipo desconhecido intencionalmente; faça narrowing no consumidor. */
|
||||
valor: unknown
|
||||
}>
|
||||
formula?: string
|
||||
}
|
||||
|
||||
function key(nomeTabela: string) {
|
||||
return `eli_tabela:${nomeTabela}:filtro_avancado`
|
||||
}
|
||||
|
||||
export function carregarFiltroAvancado<T>(
|
||||
nomeTabela: string,
|
||||
): EliTabelaFiltroAvancadoSalvo<T> {
|
||||
/**
|
||||
* Carrega do `localStorage` as linhas de filtro avançado salvas para uma tabela.
|
||||
* Trata dois formatos legados: array direto de filtros (v1) e objeto `{ linhas, formula }` (v2).
|
||||
* Em caso de dado ausente, corrompido ou formato desconhecido, retorna estado vazio.
|
||||
*
|
||||
* @param nomeTabela - Identificador único da tabela (campo `nome` de `tipoEliTabelaConsulta`)
|
||||
* @returns Estado de filtros salvo, ou `{ linhas: [], formula: '' }` como fallback seguro
|
||||
*/
|
||||
export function carregarFiltroAvancado<T>(nomeTabela: string): EliTabelaFiltroAvancadoSalvo<T> {
|
||||
const vazio: EliTabelaFiltroAvancadoSalvo<T> = { linhas: [], formula: "" }
|
||||
try {
|
||||
const raw = localStorage.getItem(key(nomeTabela))
|
||||
if (!raw) return [] as unknown as EliTabelaFiltroAvancadoSalvo<T>
|
||||
if (!raw) return vazio
|
||||
|
||||
const parsed = JSON.parse(raw)
|
||||
return Array.isArray(parsed)
|
||||
? (parsed as EliTabelaFiltroAvancadoSalvo<T>)
|
||||
: ([] as unknown as EliTabelaFiltroAvancadoSalvo<T>)
|
||||
if (Array.isArray(parsed)) {
|
||||
return { linhas: parsed, formula: "" } as EliTabelaFiltroAvancadoSalvo<T>
|
||||
}
|
||||
|
||||
if (parsed && typeof parsed === "object") {
|
||||
return {
|
||||
linhas: Array.isArray(parsed.linhas) ? parsed.linhas : [],
|
||||
formula: typeof parsed.formula === "string" ? parsed.formula : "",
|
||||
} as EliTabelaFiltroAvancadoSalvo<T>
|
||||
}
|
||||
|
||||
return vazio
|
||||
} catch {
|
||||
return [] as unknown as EliTabelaFiltroAvancadoSalvo<T>
|
||||
return vazio
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Persiste no `localStorage` o estado do filtro avançado de uma tabela.
|
||||
* Salva tanto as linhas de filtro quanto a fórmula booleana customizada.
|
||||
* Falhas de armazenamento são silenciadas para não interromper o fluxo do usuário.
|
||||
*
|
||||
* @param nomeTabela - Identificador único da tabela
|
||||
* @param filtros - Estado completo dos filtros a persistir
|
||||
*/
|
||||
export function salvarFiltroAvancado<T>(
|
||||
nomeTabela: string,
|
||||
filtros: EliTabelaFiltroAvancadoSalvo<T>,
|
||||
) {
|
||||
try {
|
||||
localStorage.setItem(key(nomeTabela), JSON.stringify(filtros ?? []))
|
||||
localStorage.setItem(key(nomeTabela), JSON.stringify(filtros ?? { linhas: [] }))
|
||||
} catch {
|
||||
// ignore
|
||||
// Silencia erros de storage (modo privado, quota excedida)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove do `localStorage` os filtros avançados salvos para uma tabela.
|
||||
* Chamado ao executar "Limpar Filtros" no modal de filtro avançado.
|
||||
*
|
||||
* @param nomeTabela - Identificador único da tabela
|
||||
*/
|
||||
export function limparFiltroAvancado(nomeTabela: string) {
|
||||
try {
|
||||
localStorage.removeItem(key(nomeTabela))
|
||||
} catch {
|
||||
// ignore
|
||||
// Silencia erros de storage
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,14 +1,9 @@
|
|||
import type { LucideIcon } from "@lucide/vue"
|
||||
import type { tipoFiltro26 } from "p-comuns"
|
||||
|
||||
export type tipoFiltro<T> = tipoFiltro26<T>
|
||||
import type { tipoFiltroDeltaColunaTipada } from "p-comuns"
|
||||
|
||||
import type { tipoResposta } from "p-respostas"
|
||||
import type { ComponenteEntrada } from "../EliEntrada/tiposEntradas"
|
||||
import type {
|
||||
tiposTabelaCelulas,
|
||||
tipoTabelaCelula,
|
||||
} from "./celulas/tiposTabelaCelulas"
|
||||
import type { tiposTabelaCelulas, tipoTabelaCelula } from "./celulas/tiposTabelaCelulas"
|
||||
|
||||
export type tipoComponenteCelulaBase<T extends tipoTabelaCelula> = readonly [
|
||||
T,
|
||||
|
|
@ -19,6 +14,20 @@ export type tipoComponenteCelula = {
|
|||
[K in tipoTabelaCelula]: tipoComponenteCelulaBase<K>
|
||||
}[tipoTabelaCelula]
|
||||
|
||||
/**
|
||||
* Cria uma tupla tipada `[tipo, dados]` que representa o conteúdo de uma célula da tabela.
|
||||
* Funciona como type-safe builder: o TypeScript infere o tipo exato de `dados` a partir de `tipo`,
|
||||
* garantindo que não seja possível passar dados incorretos para um tipo de célula.
|
||||
*
|
||||
* Uso esperado dentro de `tipoEliColuna.celula`:
|
||||
* ```ts
|
||||
* celula: (linha) => celulaTabela("textoSimples", { texto: linha.nome })
|
||||
* ```
|
||||
*
|
||||
* @param tipo - Identificador do tipo de célula (chave de `tiposTabelaCelulas`)
|
||||
* @param dados - Dados tipados para o tipo de célula escolhido
|
||||
* @returns Tupla `[tipo, dados]` consumida pelo `EliTabelaCelula.vue`
|
||||
*/
|
||||
export const celulaTabela = <T extends tipoTabelaCelula>(
|
||||
tipo: T,
|
||||
dados: tiposTabelaCelulas[T],
|
||||
|
|
@ -72,7 +81,7 @@ export type tipoEliTabelaAcao<T> = {
|
|||
}
|
||||
|
||||
export type parametrosConsulta<T> = {
|
||||
filtros?: tipoFiltro26<T>[]
|
||||
filtros?: tipoFiltroDeltaColunaTipada<Extract<keyof T, string>>[]
|
||||
coluna_ordem?: keyof T
|
||||
direcao_ordem?: "asc" | "desc"
|
||||
offSet?: number
|
||||
|
|
@ -152,7 +161,7 @@ export type tipoEliTabelaConsulta<T> = {
|
|||
/** Rótulo exibido no select de adicionar filtro */
|
||||
rotulo: string
|
||||
/** vai gerar filtro e mescla com os filtros já existentes */
|
||||
filtro: (valor: unknown) => tipoFiltro26<T>
|
||||
filtro: (valor: unknown) => tipoFiltroDeltaColunaTipada<Extract<keyof T, string>> | undefined
|
||||
/** componente de entrada que vai receber o valor para o filtro */
|
||||
entrada: ComponenteEntrada
|
||||
}[]
|
||||
|
|
|
|||
139
src/componentes/EliTabela/useColunasOrdenacao.ts
Normal file
139
src/componentes/EliTabela/useColunasOrdenacao.ts
Normal file
|
|
@ -0,0 +1,139 @@
|
|||
/**
|
||||
* useColunasOrdenacao
|
||||
* Composable que encapsula a lógica de drag-and-drop e sincronização de estado
|
||||
* do modal de configuração de colunas da EliTabela.
|
||||
*
|
||||
* Responsável por:
|
||||
* - Sincronizar colunas visíveis/invisíveis com a configuração persistida
|
||||
* - Aplicar ordem salva e adicionar novas colunas ao final
|
||||
* - Gerenciar eventos de arrastar-e-soltar entre as duas listas
|
||||
*
|
||||
* Extraído de `EliTabelaModalColunas.vue` para manter o componente abaixo de 250 linhas.
|
||||
*/
|
||||
import { type Ref, ref, watch } from "vue"
|
||||
import type { EliTabelaColunasConfig } from "./colunasStorage"
|
||||
import type { tipoEliColuna } from "./types-eli-tabela"
|
||||
|
||||
type OrigemLista = "visiveis" | "invisiveis"
|
||||
|
||||
interface UseColunasOrdenacaoOptions {
|
||||
aberto: Ref<boolean>
|
||||
rotulosColunas: Ref<string[]>
|
||||
configInicial: Ref<EliTabelaColunasConfig>
|
||||
// biome-ignore lint/suspicious/noExplicitAny: colunas genéricas — tipo parametrizado no pai
|
||||
colunas: Ref<tipoEliColuna<any>[]>
|
||||
}
|
||||
|
||||
/**
|
||||
* Composable de drag-and-drop para reordenação e alternância de visibilidade de colunas.
|
||||
* Sincroniza automaticamente quando `aberto`, `rotulosColunas`, `configInicial` ou `colunas` mudam.
|
||||
*
|
||||
* @param options.aberto - Ref que controla se o modal está aberto (gatilho de sincronização)
|
||||
* @param options.rotulosColunas - Ref com todos os rótulos de colunas da tabela
|
||||
* @param options.configInicial - Ref com a configuração persistida (visíveis + invisíveis)
|
||||
* @param options.colunas - Ref com a definição completa das colunas (para ler `visivel: false` padrão)
|
||||
* @returns Estado reativo e handlers de drag-and-drop prontos para o template
|
||||
*/
|
||||
export function useColunasOrdenacao({
|
||||
aberto,
|
||||
rotulosColunas,
|
||||
configInicial,
|
||||
colunas,
|
||||
}: UseColunasOrdenacaoOptions) {
|
||||
const visiveisLocal = ref<string[]>([])
|
||||
const invisiveisLocal = ref<string[]>([])
|
||||
const itemSendoArrastado = ref<string | null>(null)
|
||||
|
||||
function sincronizarEstado() {
|
||||
const todos = rotulosColunas.value
|
||||
const configTemDados =
|
||||
(configInicial.value.visiveis?.length ?? 0) > 0 ||
|
||||
(configInicial.value.invisiveis?.length ?? 0) > 0
|
||||
|
||||
const invisiveisPadraoSet = new Set(
|
||||
colunas.value.filter((c) => c.visivel === false).map((c) => c.rotulo),
|
||||
)
|
||||
const invisiveisSet = configTemDados
|
||||
? new Set(configInicial.value.invisiveis ?? [])
|
||||
: invisiveisPadraoSet
|
||||
|
||||
const baseVisiveis = todos.filter((r) => !invisiveisSet.has(r))
|
||||
const ordemSalva = configInicial.value.visiveis ?? []
|
||||
const setVis = new Set(baseVisiveis)
|
||||
const ordenadas: string[] = []
|
||||
|
||||
for (const r of ordemSalva) {
|
||||
if (setVis.has(r)) ordenadas.push(r)
|
||||
}
|
||||
for (const r of baseVisiveis) {
|
||||
if (!ordenadas.includes(r)) ordenadas.push(r)
|
||||
}
|
||||
|
||||
visiveisLocal.value = ordenadas
|
||||
invisiveisLocal.value = todos.filter((r) => invisiveisSet.has(r))
|
||||
}
|
||||
|
||||
watch(
|
||||
() => [aberto.value, rotulosColunas.value, configInicial.value, colunas.value] as const,
|
||||
() => {
|
||||
if (aberto.value) sincronizarEstado()
|
||||
},
|
||||
{ deep: true, immediate: true },
|
||||
)
|
||||
|
||||
function onDragStart(e: DragEvent, rotulo: string) {
|
||||
itemSendoArrastado.value = rotulo
|
||||
if (e.dataTransfer) {
|
||||
e.dataTransfer.setData("text/plain", rotulo)
|
||||
e.dataTransfer.effectAllowed = "move"
|
||||
}
|
||||
}
|
||||
|
||||
function onDragEnd() {
|
||||
itemSendoArrastado.value = null
|
||||
}
|
||||
|
||||
function onDragOverItem(alvoRotulo: string, destino: OrigemLista) {
|
||||
const draggedRotulo = itemSendoArrastado.value
|
||||
if (!draggedRotulo || draggedRotulo === alvoRotulo) return
|
||||
|
||||
const origemVisIdx = visiveisLocal.value.indexOf(draggedRotulo)
|
||||
const origemInvIdx = invisiveisLocal.value.indexOf(draggedRotulo)
|
||||
const origem = origemVisIdx >= 0 ? "visiveis" : origemInvIdx >= 0 ? "invisiveis" : null
|
||||
if (!origem) return
|
||||
|
||||
const origemArr = origem === "visiveis" ? visiveisLocal.value : invisiveisLocal.value
|
||||
const destinoArr = destino === "visiveis" ? visiveisLocal.value : invisiveisLocal.value
|
||||
|
||||
const dragIdx = origemArr.indexOf(draggedRotulo)
|
||||
const alvoIdx = destinoArr.indexOf(alvoRotulo)
|
||||
|
||||
origemArr.splice(dragIdx, 1)
|
||||
destinoArr.splice(alvoIdx, 0, draggedRotulo)
|
||||
}
|
||||
|
||||
function onDragOverLista(destino: OrigemLista) {
|
||||
const draggedRotulo = itemSendoArrastado.value
|
||||
if (!draggedRotulo) return
|
||||
|
||||
const destinoArr = destino === "visiveis" ? visiveisLocal.value : invisiveisLocal.value
|
||||
if (destinoArr[destinoArr.length - 1] === draggedRotulo) return
|
||||
|
||||
const origemVisIdx = visiveisLocal.value.indexOf(draggedRotulo)
|
||||
const origemInvIdx = invisiveisLocal.value.indexOf(draggedRotulo)
|
||||
if (origemVisIdx >= 0) visiveisLocal.value.splice(origemVisIdx, 1)
|
||||
if (origemInvIdx >= 0) invisiveisLocal.value.splice(origemInvIdx, 1)
|
||||
|
||||
destinoArr.push(draggedRotulo)
|
||||
}
|
||||
|
||||
return {
|
||||
visiveisLocal,
|
||||
invisiveisLocal,
|
||||
itemSendoArrastado,
|
||||
onDragStart,
|
||||
onDragEnd,
|
||||
onDragOverItem,
|
||||
onDragOverLista,
|
||||
}
|
||||
}
|
||||
105
src/componentes/EliTabela/useEliTabela.ts
Normal file
105
src/componentes/EliTabela/useEliTabela.ts
Normal file
|
|
@ -0,0 +1,105 @@
|
|||
/**
|
||||
* useEliTabela
|
||||
* Orquestrador principal da EliTabela — compõe todos os sub-composables
|
||||
* e retorna o estado unificado para o template do componente.
|
||||
*
|
||||
* Composables compostos:
|
||||
* - `useEliTabelaConsulta` — carregamento, paginação, ordenação, busca
|
||||
* - `useEliTabelaFiltrosAvancados` — filtros avançados, modal e persistência
|
||||
* - `useEliTabelaSelecao` — seleção múltipla de linhas
|
||||
* - `useEliTabelaColunas` — visibilidade, ordem e modal de colunas
|
||||
* - `useEliTabelaMenu` — menu de ações por linha e posicionamento de popup
|
||||
*
|
||||
* @see EliTabela.vue — componente que consome este composable
|
||||
*/
|
||||
import { onMounted, ref, watch } from "vue"
|
||||
import { carregarConfigColunas } from "./colunasStorage"
|
||||
import { carregarFiltroAvancado } from "./filtroAvancadoStorage"
|
||||
import type { tipoEliTabelaConsulta } from "./types-eli-tabela"
|
||||
import { useEliTabelaColunas } from "./useEliTabelaColunas"
|
||||
import { useEliTabelaConsulta } from "./useEliTabelaConsulta"
|
||||
import { useEliTabelaFiltrosAvancados } from "./useEliTabelaFiltrosAvancados"
|
||||
import { useEliTabelaMenu } from "./useEliTabelaMenu"
|
||||
import { useEliTabelaSelecao } from "./useEliTabelaSelecao"
|
||||
|
||||
// biome-ignore lint/suspicious/noExplicitAny: dynamic table — genérico passado por prop
|
||||
type TabelaProps = tipoEliTabelaConsulta<any>
|
||||
|
||||
export function useEliTabela(
|
||||
props: { tabela: TabelaProps; selecionados: unknown[]; esconderDebug: boolean },
|
||||
emit: (event: "update:selecionados", value: unknown[]) => void,
|
||||
) {
|
||||
// Estado compartilhado entre sub-composables (passado por ref para reatividade)
|
||||
const paginaAtual = ref(1)
|
||||
const valorBusca = ref<string>("")
|
||||
|
||||
// Filtros avançados (precisa de carregar que ainda não existe; passa proxy que usará o da consulta)
|
||||
// Usamos um ref de função para quebrar a dependência circular: filtros precisam de carregar,
|
||||
// e consulta precisa de filtrosAvancadosAtivos.
|
||||
let _carregarProxy: () => Promise<void> = async () => {
|
||||
/* será atribuído após consulta ser criada */
|
||||
}
|
||||
const carregarProxy = () => _carregarProxy()
|
||||
|
||||
const filtros = useEliTabelaFiltrosAvancados(props, paginaAtual, valorBusca, carregarProxy)
|
||||
|
||||
// Colunas: temAcoes vem da consulta — usamos um ref intermediário
|
||||
const _temAcoesRef = ref(false)
|
||||
const colunas = useEliTabelaColunas(props, _temAcoesRef)
|
||||
|
||||
const consulta = useEliTabelaConsulta(
|
||||
props,
|
||||
filtros.filtrosAvancadosAtivos,
|
||||
paginaAtual,
|
||||
valorBusca,
|
||||
colunas.linhasExpandidas,
|
||||
)
|
||||
|
||||
// Conecta o proxy ao carregar real
|
||||
_carregarProxy = consulta.carregar
|
||||
// Sincroniza temAcoes para o composable de colunas
|
||||
// biome-ignore lint/suspicious/noExplicitAny: ref sync interno
|
||||
;(_temAcoesRef as any).value = consulta.temAcoes.value
|
||||
watch(consulta.temAcoes, (v) => {
|
||||
_temAcoesRef.value = v
|
||||
})
|
||||
|
||||
const selecao = useEliTabelaSelecao(props, emit, consulta.linhasPaginadas)
|
||||
const menu = useEliTabelaMenu(props, consulta.acoesVisiveis)
|
||||
|
||||
// Watch: troca de configuração de tabela reseta todos os estados e recarrega
|
||||
watch(
|
||||
() => props.tabela,
|
||||
() => {
|
||||
menu.menuAberto.value = null
|
||||
consulta.colunaOrdenacao.value = null
|
||||
consulta.direcaoOrdenacao.value = "asc"
|
||||
valorBusca.value = ""
|
||||
colunas.modalColunasAberto.value = false
|
||||
filtros.modalFiltroAberto.value = false
|
||||
colunas.configColunas.value = carregarConfigColunas(props.tabela.nome)
|
||||
const filtrosRecarregados = carregarFiltroAvancado<Record<string, unknown>>(props.tabela.nome)
|
||||
filtros.filtrosUi.value = filtrosRecarregados.linhas
|
||||
colunas.linhasExpandidas.value = {}
|
||||
if (paginaAtual.value !== 1) paginaAtual.value = 1
|
||||
else void consulta.carregar()
|
||||
},
|
||||
)
|
||||
|
||||
onMounted(() => void consulta.carregar())
|
||||
|
||||
return {
|
||||
...consulta,
|
||||
...filtros,
|
||||
...selecao,
|
||||
...colunas,
|
||||
menuAberto: menu.menuAberto,
|
||||
menuPopup: menu.menuPopup,
|
||||
menuPopupPos: menu.menuPopupPos,
|
||||
possuiAcoes: menu.possuiAcoes,
|
||||
toggleMenu: menu.toggleMenu,
|
||||
acoesDisponiveisPorLinha: menu.acoesDisponiveisPorLinha,
|
||||
paginaAtual,
|
||||
valorBusca,
|
||||
}
|
||||
}
|
||||
129
src/componentes/EliTabela/useEliTabelaColunas.ts
Normal file
129
src/componentes/EliTabela/useEliTabelaColunas.ts
Normal file
|
|
@ -0,0 +1,129 @@
|
|||
/**
|
||||
* useEliTabelaColunas
|
||||
* Gerencia visibilidade, ordenação e persistência da configuração de colunas da EliTabela.
|
||||
* Controla também o modal de colunas e o estado de linhas expandidas (detalhe de colunas invisíveis).
|
||||
*
|
||||
* @param props - Props do componente (tabela.colunas, tabela.nome)
|
||||
* @param temAcoes - Computed que indica se existem ações por linha (para totalColunas)
|
||||
*/
|
||||
import { computed, type Ref, ref } from "vue"
|
||||
import {
|
||||
carregarConfigColunas,
|
||||
type EliTabelaColunasConfig,
|
||||
salvarConfigColunas,
|
||||
} from "./colunasStorage"
|
||||
import type { tipoEliColuna } from "./types-eli-tabela"
|
||||
|
||||
// biome-ignore lint/suspicious/noExplicitAny: colunas dinâmicas — genérico passado por prop
|
||||
type PropsColunas = { tabela: { colunas: tipoEliColuna<any>[]; nome: string } }
|
||||
|
||||
export function useEliTabelaColunas(props: PropsColunas, temAcoes: Ref<boolean>) {
|
||||
const modalColunasAberto = ref(false)
|
||||
const configColunas = ref<EliTabelaColunasConfig>(carregarConfigColunas(props.tabela.nome))
|
||||
const linhasExpandidas = ref<Record<number, boolean>>({})
|
||||
|
||||
const rotulosColunas = computed(() => props.tabela.colunas.map((c) => c.rotulo))
|
||||
|
||||
const colunasInvisiveisEfetivas = computed(() => {
|
||||
// biome-ignore lint/suspicious/noExplicitAny: dynamic column
|
||||
const colunas = props.tabela.colunas as tipoEliColuna<any>[]
|
||||
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))
|
||||
// biome-ignore lint/suspicious/noExplicitAny: dynamic map
|
||||
const mapa = new Map<string, tipoEliColuna<any>>()
|
||||
for (const c of base) {
|
||||
if (!mapa.has(c.rotulo)) mapa.set(c.rotulo, c)
|
||||
}
|
||||
// biome-ignore lint/suspicious/noExplicitAny: dynamic column
|
||||
const ordenadas: tipoEliColuna<any>[] = []
|
||||
for (const r of invisiveisBaseRotulos) {
|
||||
const c = mapa.get(r)
|
||||
if (c) ordenadas.push(c)
|
||||
}
|
||||
for (const c of base) {
|
||||
if (!ordenadas.includes(c)) ordenadas.push(c)
|
||||
}
|
||||
return ordenadas
|
||||
})
|
||||
|
||||
const temColunasInvisiveis = computed(() => colunasInvisiveisEfetivas.value.length > 0)
|
||||
|
||||
const colunasEfetivas = computed(() => {
|
||||
const colunas = props.tabela.colunas
|
||||
const todosRotulos = rotulosColunas.value
|
||||
const configTemDados =
|
||||
(configColunas.value.visiveis?.length ?? 0) > 0 ||
|
||||
(configColunas.value.invisiveis?.length ?? 0) > 0
|
||||
const invisiveisBaseRotulos = configTemDados
|
||||
? (configColunas.value.invisiveis ?? [])
|
||||
: // biome-ignore lint/suspicious/noExplicitAny: dynamic column
|
||||
(props.tabela.colunas as tipoEliColuna<any>[])
|
||||
.filter((c) => c.visivel === false)
|
||||
.map((c) => c.rotulo)
|
||||
const invisiveisSet = new Set(invisiveisBaseRotulos)
|
||||
const visiveisBaseRotulos = todosRotulos.filter((r) => !invisiveisSet.has(r))
|
||||
const visiveisSet = new Set(visiveisBaseRotulos)
|
||||
const ordemSalva = configTemDados ? (configColunas.value.visiveis ?? []) : []
|
||||
const ordemFinal: string[] = []
|
||||
for (const r of ordemSalva) {
|
||||
if (visiveisSet.has(r)) ordemFinal.push(r)
|
||||
}
|
||||
for (const r of visiveisBaseRotulos) {
|
||||
if (!ordemFinal.includes(r)) ordemFinal.push(r)
|
||||
}
|
||||
// biome-ignore lint/suspicious/noExplicitAny: dynamic map
|
||||
const mapa = new Map<string, any>()
|
||||
for (const c of colunas) {
|
||||
if (!mapa.has(c.rotulo)) mapa.set(c.rotulo, c)
|
||||
}
|
||||
return ordemFinal.map((r) => mapa.get(r)).filter(Boolean)
|
||||
})
|
||||
|
||||
const totalColunas = computed(
|
||||
() =>
|
||||
colunasEfetivas.value.length +
|
||||
(temAcoes.value ? 1 : 0) +
|
||||
(temColunasInvisiveis.value ? 1 : 0),
|
||||
)
|
||||
|
||||
function abrirModalColunas() {
|
||||
modalColunasAberto.value = true
|
||||
}
|
||||
|
||||
function fecharModalColunas() {
|
||||
modalColunasAberto.value = false
|
||||
}
|
||||
|
||||
function salvarModalColunas(cfg: EliTabelaColunasConfig) {
|
||||
configColunas.value = cfg
|
||||
salvarConfigColunas(props.tabela.nome, cfg)
|
||||
modalColunasAberto.value = false
|
||||
linhasExpandidas.value = {}
|
||||
}
|
||||
|
||||
function alternarLinhaExpandida(indice: number) {
|
||||
const atual = Boolean(linhasExpandidas.value[indice])
|
||||
linhasExpandidas.value = { ...linhasExpandidas.value, [indice]: !atual }
|
||||
}
|
||||
|
||||
return {
|
||||
modalColunasAberto,
|
||||
configColunas,
|
||||
linhasExpandidas,
|
||||
rotulosColunas,
|
||||
colunasEfetivas,
|
||||
colunasInvisiveisEfetivas,
|
||||
temColunasInvisiveis,
|
||||
totalColunas,
|
||||
abrirModalColunas,
|
||||
fecharModalColunas,
|
||||
salvarModalColunas,
|
||||
alternarLinhaExpandida,
|
||||
}
|
||||
}
|
||||
247
src/componentes/EliTabela/useEliTabelaConsulta.ts
Normal file
247
src/componentes/EliTabela/useEliTabelaConsulta.ts
Normal file
|
|
@ -0,0 +1,247 @@
|
|||
/**
|
||||
* useEliTabelaConsulta
|
||||
* Gerencia o ciclo de consultas da EliTabela: carregamento, paginação, ordenação e busca.
|
||||
* Usa um contador sequencial para descartar respostas de consultas antigas (race condition).
|
||||
*
|
||||
* @param props - Props do componente (tabela, esconderDebug)
|
||||
* @param filtrosAvancadosAtivos - Computed com filtros ativos para envio à API
|
||||
* @param paginaAtual - Ref da página atual (compartilhada com filtros e busca)
|
||||
* @param valorBusca - Ref da busca textual (compartilhada com filtros avançados)
|
||||
* @param linhasExpandidas - Ref das linhas expandidas (resetada ao carregar)
|
||||
*/
|
||||
import type { tipoFiltroDeltaColunaTipada } from "p-comuns"
|
||||
import { codigosResposta } from "p-respostas"
|
||||
import { computed, type Ref, ref, watch } from "vue"
|
||||
import type { parametrosConsulta, tipoEliTabelaConsulta } from "./types-eli-tabela"
|
||||
|
||||
// biome-ignore lint/suspicious/noExplicitAny: genérico passado por prop
|
||||
type PropsConsulta = { tabela: tipoEliTabelaConsulta<any>; esconderDebug: boolean }
|
||||
|
||||
export function useEliTabelaConsulta(
|
||||
props: PropsConsulta,
|
||||
// biome-ignore lint/suspicious/noExplicitAny: genérico
|
||||
filtrosAvancadosAtivos: Ref<tipoFiltroDeltaColunaTipada<any>[]>,
|
||||
paginaAtual: Ref<number>,
|
||||
valorBusca: Ref<string>,
|
||||
linhasExpandidas: Ref<Record<number, boolean>>,
|
||||
) {
|
||||
const isDev = import.meta.env.DEV && !props.esconderDebug
|
||||
const carregando = ref(false)
|
||||
const erro = ref<string | null>(null)
|
||||
const linhas = ref<unknown[]>([])
|
||||
const quantidade = ref<number>(0)
|
||||
const acoesVisiveis = ref<boolean[][]>([])
|
||||
const colunaOrdenacao = ref<string | null>(null)
|
||||
const direcaoOrdenacao = ref<"asc" | "desc">("asc")
|
||||
|
||||
const tabela = computed(() => props.tabela)
|
||||
const registrosPorConsulta = computed(() => {
|
||||
const valor = props.tabela.registros_por_consulta
|
||||
return typeof valor === "number" && valor > 0 ? Math.floor(valor) : 10
|
||||
})
|
||||
const totalPaginas = computed(() => {
|
||||
const limite = registrosPorConsulta.value
|
||||
if (!limite || limite <= 0) return 1
|
||||
const total = quantidade.value ?? 0
|
||||
return total ? Math.max(1, Math.ceil(total / limite)) : 1
|
||||
})
|
||||
const linhasPaginadas = computed(() => linhas.value ?? [])
|
||||
const quantidadeFiltrada = computed(() => quantidade.value ?? 0)
|
||||
const temAcoes = computed(() => (props.tabela.acoesLinha ?? []).length > 0)
|
||||
const exibirBusca = computed(() => Boolean(props.tabela.mostrarCaixaDeBusca))
|
||||
const acoesSuperiores = computed(() =>
|
||||
(props.tabela.acoesTabela ?? []).filter((a) => a.posicao === "superior"),
|
||||
)
|
||||
const acoesInferiores = computed(() =>
|
||||
(props.tabela.acoesTabela ?? []).filter((a) => a.posicao === "inferior"),
|
||||
)
|
||||
const temAcoesSuperiores = computed(() => acoesSuperiores.value.length > 0)
|
||||
const temAcoesInferiores = computed(() => acoesInferiores.value.length > 0)
|
||||
const parametrosConsultaAtuais = computed(() => {
|
||||
const limite = Math.max(1, registrosPorConsulta.value)
|
||||
const offset = (paginaAtual.value - 1) * limite
|
||||
// biome-ignore lint/suspicious/noExplicitAny: dynamic params
|
||||
const params: parametrosConsulta<any> = { offSet: offset, limit: limite }
|
||||
const busca = (valorBusca.value ?? "").trim()
|
||||
if (busca) {
|
||||
params.texto_busca = busca
|
||||
} else {
|
||||
const filtros = filtrosAvancadosAtivos.value
|
||||
if (filtros.length) params.filtros = filtros
|
||||
}
|
||||
if (colunaOrdenacao.value) {
|
||||
params.coluna_ordem = colunaOrdenacao.value
|
||||
params.direcao_ordem = direcaoOrdenacao.value
|
||||
}
|
||||
return {
|
||||
...params,
|
||||
atualizarConsulta: async () => {
|
||||
await carregar()
|
||||
},
|
||||
// biome-ignore lint/suspicious/noExplicitAny: dynamic list update
|
||||
editarLista: async (novaLista: any[]) => {
|
||||
linhas.value = novaLista
|
||||
return novaLista
|
||||
},
|
||||
}
|
||||
})
|
||||
|
||||
let carregamentoSequencial = 0
|
||||
|
||||
async function carregar() {
|
||||
const idCarregamento = ++carregamentoSequencial
|
||||
carregando.value = true
|
||||
erro.value = null
|
||||
try {
|
||||
acoesVisiveis.value = []
|
||||
linhasExpandidas.value = {}
|
||||
const limite = Math.max(1, registrosPorConsulta.value)
|
||||
const offset = (paginaAtual.value - 1) * limite
|
||||
const parametros: {
|
||||
// biome-ignore lint/suspicious/noExplicitAny: generic
|
||||
filtros?: tipoFiltroDeltaColunaTipada<any>[]
|
||||
coluna_ordem?: never
|
||||
direcao_ordem?: "asc" | "desc"
|
||||
offSet: number
|
||||
limit: number
|
||||
texto_busca?: string
|
||||
} = { offSet: offset, limit: limite }
|
||||
const busca = (valorBusca.value ?? "").trim()
|
||||
if (busca) {
|
||||
parametros.texto_busca = busca
|
||||
} else {
|
||||
const filtros = filtrosAvancadosAtivos.value
|
||||
if (filtros.length) parametros.filtros = filtros
|
||||
}
|
||||
if (colunaOrdenacao.value) {
|
||||
parametros.coluna_ordem = colunaOrdenacao.value as never
|
||||
parametros.direcao_ordem = direcaoOrdenacao.value
|
||||
}
|
||||
if (isDev) console.log("[EliTabela] consulta(parametros)", parametros) // eslint-disable-line no-console
|
||||
const res = await props.tabela.consulta(parametros)
|
||||
if (idCarregamento !== carregamentoSequencial) return
|
||||
if (res.cod !== codigosResposta.sucesso) {
|
||||
linhas.value = []
|
||||
quantidade.value = 0
|
||||
erro.value = res.mensagem
|
||||
return
|
||||
}
|
||||
const valores = res.valor?.valores ?? []
|
||||
const total = res.valor?.quantidade ?? valores.length
|
||||
linhas.value = valores
|
||||
quantidade.value = Number(total) || 0
|
||||
const totalPaginasRecalculado = Math.max(1, Math.ceil((quantidade.value || 0) / limite))
|
||||
if (paginaAtual.value > totalPaginasRecalculado) paginaAtual.value = totalPaginasRecalculado
|
||||
const acoesConf = props.tabela.acoesLinha ?? []
|
||||
if (!acoesConf.length) {
|
||||
acoesVisiveis.value = []
|
||||
return
|
||||
}
|
||||
acoesVisiveis.value = valores.map(() =>
|
||||
acoesConf.map((a) =>
|
||||
a.exibir === undefined ? true : typeof a.exibir === "boolean" ? a.exibir : false,
|
||||
),
|
||||
)
|
||||
const visibilidade = await Promise.all(
|
||||
valores.map((linha) =>
|
||||
Promise.all(
|
||||
acoesConf.map(async (acao) => {
|
||||
if (acao.exibir === undefined) return true
|
||||
if (typeof acao.exibir === "boolean") return acao.exibir
|
||||
try {
|
||||
return Boolean(await Promise.resolve(acao.exibir(linha as never)))
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}),
|
||||
),
|
||||
),
|
||||
)
|
||||
if (idCarregamento === carregamentoSequencial) acoesVisiveis.value = visibilidade
|
||||
} catch (e) {
|
||||
console.error("[EliTabela] Erro capturado em carregar():", e)
|
||||
if (idCarregamento !== carregamentoSequencial) return
|
||||
linhas.value = []
|
||||
quantidade.value = 0
|
||||
erro.value = e instanceof Error ? e.message : "Erro ao carregar dados."
|
||||
} finally {
|
||||
if (idCarregamento === carregamentoSequencial) carregando.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function alternarOrdenacao(chave?: string) {
|
||||
if (!chave) return
|
||||
if (colunaOrdenacao.value === chave) {
|
||||
direcaoOrdenacao.value = direcaoOrdenacao.value === "asc" ? "desc" : "asc"
|
||||
void carregar()
|
||||
return
|
||||
}
|
||||
colunaOrdenacao.value = chave
|
||||
direcaoOrdenacao.value = "asc"
|
||||
if (paginaAtual.value !== 1) paginaAtual.value = 1
|
||||
else void carregar()
|
||||
}
|
||||
|
||||
function atualizarBusca(texto: string) {
|
||||
if (valorBusca.value === texto) return
|
||||
valorBusca.value = texto
|
||||
if (paginaAtual.value !== 1) paginaAtual.value = 1
|
||||
else void carregar()
|
||||
}
|
||||
|
||||
function irParaPagina(pagina: number) {
|
||||
const alvo = Math.min(Math.max(1, pagina), totalPaginas.value)
|
||||
if (alvo !== paginaAtual.value) paginaAtual.value = alvo
|
||||
}
|
||||
|
||||
watch(paginaAtual, (nova, antiga) => {
|
||||
if (nova !== antiga) void carregar()
|
||||
})
|
||||
watch(linhas, () => {
|
||||
linhasExpandidas.value = {}
|
||||
})
|
||||
watch(
|
||||
() => props.tabela.mostrarCaixaDeBusca,
|
||||
(mostrar) => {
|
||||
if (!mostrar && valorBusca.value) {
|
||||
valorBusca.value = ""
|
||||
if (paginaAtual.value !== 1) paginaAtual.value = 1
|
||||
else void carregar()
|
||||
}
|
||||
},
|
||||
)
|
||||
watch(
|
||||
() => props.tabela.registros_por_consulta,
|
||||
() => {
|
||||
if (paginaAtual.value !== 1) paginaAtual.value = 1
|
||||
else void carregar()
|
||||
},
|
||||
)
|
||||
|
||||
return {
|
||||
isDev,
|
||||
tabela,
|
||||
carregando,
|
||||
erro,
|
||||
linhas,
|
||||
linhasPaginadas,
|
||||
quantidade,
|
||||
quantidadeFiltrada,
|
||||
acoesVisiveis,
|
||||
colunaOrdenacao,
|
||||
direcaoOrdenacao,
|
||||
registrosPorConsulta,
|
||||
totalPaginas,
|
||||
temAcoes,
|
||||
exibirBusca,
|
||||
acoesSuperiores,
|
||||
acoesInferiores,
|
||||
temAcoesSuperiores,
|
||||
temAcoesInferiores,
|
||||
parametrosConsultaAtuais,
|
||||
carregar,
|
||||
alternarOrdenacao,
|
||||
atualizarBusca,
|
||||
irParaPagina,
|
||||
}
|
||||
}
|
||||
128
src/componentes/EliTabela/useEliTabelaFiltrosAvancados.ts
Normal file
128
src/componentes/EliTabela/useEliTabelaFiltrosAvancados.ts
Normal file
|
|
@ -0,0 +1,128 @@
|
|||
/**
|
||||
* useEliTabelaFiltrosAvancados
|
||||
* Gerencia o estado de filtros avançados da EliTabela, incluindo:
|
||||
* - Linhas de filtro ativas (`filtrosUi`) e fórmula booleana (`filtrosFormula`)
|
||||
* - Computed `filtrosAvancadosAtivos` que converte as linhas em FiltroDelta para a API
|
||||
* - Abertura/fechamento e limpeza do modal de filtro avançado
|
||||
*
|
||||
* A fórmula é armazenada no formato `{chave}` (ex: `{email} E {nome}`) e convertida
|
||||
* para IDs globais `{c1} {c2}` durante a montagem do FiltroDelta final.
|
||||
*
|
||||
* @param props - Props do componente (tabela.filtroAvancado, tabela.nome)
|
||||
* @param paginaAtual - Ref da página atual (para resetar ao aplicar filtros)
|
||||
* @param valorBusca - Ref da busca textual (limpa ao aplicar filtros avançados)
|
||||
* @param carregar - Função de recarregamento dos dados
|
||||
*/
|
||||
import type { tipoCondicaoFiltroDelta, tipoFiltroDeltaColunaTipada } from "p-comuns"
|
||||
import { computed, type Ref, ref } from "vue"
|
||||
import {
|
||||
carregarFiltroAvancado,
|
||||
limparFiltroAvancado,
|
||||
salvarFiltroAvancado,
|
||||
} from "./filtroAvancadoStorage"
|
||||
|
||||
// biome-ignore lint/suspicious/noExplicitAny: dinâmico — genérico passado por prop
|
||||
type PropsFiltros = { tabela: { filtroAvancado?: any[]; nome: string } }
|
||||
|
||||
type LinhaFiltroUI = { chave: string; valor: unknown }
|
||||
|
||||
export function useEliTabelaFiltrosAvancados(
|
||||
props: PropsFiltros,
|
||||
paginaAtual: Ref<number>,
|
||||
valorBusca: Ref<string>,
|
||||
carregar: () => Promise<void>,
|
||||
) {
|
||||
const _filtrosCarregados = carregarFiltroAvancado<Record<string, unknown>>(props.tabela.nome)
|
||||
const filtrosUi = ref<LinhaFiltroUI[]>(_filtrosCarregados.linhas)
|
||||
const filtrosFormula = ref<string>(_filtrosCarregados.formula ?? "")
|
||||
const modalFiltroAberto = ref(false)
|
||||
|
||||
const exibirFiltroAvancado = computed(() => (props.tabela.filtroAvancado ?? []).length > 0)
|
||||
|
||||
// biome-ignore lint/suspicious/noExplicitAny: generic
|
||||
const filtrosAvancadosAtivos = computed<tipoFiltroDeltaColunaTipada<any>[]>(() => {
|
||||
const base = props.tabela.filtroAvancado ?? []
|
||||
const ativos = (filtrosUi.value ?? []).filter((f) => f && f.chave !== undefined)
|
||||
if (ativos.length === 0) return []
|
||||
|
||||
const filtrosIndividuais = ativos
|
||||
.map((f) => {
|
||||
const b = base.find((x) => String(x.chave) === String(f.chave))
|
||||
if (!b) return null
|
||||
return { chave: f.chave, filtro: b.filtro(f.valor) }
|
||||
})
|
||||
.filter(
|
||||
(f): f is { chave: string; filtro: tipoFiltroDeltaColunaTipada<string> } =>
|
||||
f !== null && f.filtro !== undefined && f.filtro !== null,
|
||||
)
|
||||
|
||||
if (filtrosFormula.value && filtrosIndividuais.length > 1) {
|
||||
const condicoesFinais: tipoCondicaoFiltroDelta[] = []
|
||||
let formulaFinal = filtrosFormula.value
|
||||
filtrosIndividuais.forEach(({ chave }, i) => {
|
||||
formulaFinal = formulaFinal.replace(new RegExp(`\\{${chave}\\}`, "g"), `{c${i + 1}}`)
|
||||
})
|
||||
let globalIdx = 1
|
||||
for (let i = 0; i < filtrosIndividuais.length; i++) {
|
||||
const { filtro } = filtrosIndividuais[i]
|
||||
const idMap: Record<string, string> = {}
|
||||
for (const cond of filtro.condicoes) {
|
||||
const novoId = `{c${globalIdx++}}`
|
||||
idMap[cond.id] = novoId
|
||||
condicoesFinais.push({ ...cond, id: novoId })
|
||||
}
|
||||
let formulaLocal = filtro.formula
|
||||
for (const [oldId, newId] of Object.entries(idMap)) {
|
||||
formulaLocal = formulaLocal.replace(new RegExp(`\\{${oldId.slice(1, -1)}\\}`, "g"), newId)
|
||||
}
|
||||
const subst = filtro.condicoes.length > 1 ? `(${formulaLocal})` : formulaLocal
|
||||
formulaFinal = formulaFinal.replace(new RegExp(`\\{c${i + 1}\\}`, "g"), subst)
|
||||
}
|
||||
return [{ condicoes: condicoesFinais, formula: formulaFinal }]
|
||||
}
|
||||
return filtrosIndividuais.map((f) => f.filtro)
|
||||
})
|
||||
|
||||
function abrirModalFiltro() {
|
||||
modalFiltroAberto.value = true
|
||||
}
|
||||
|
||||
function fecharModalFiltro() {
|
||||
modalFiltroAberto.value = false
|
||||
}
|
||||
|
||||
function limparFiltrosAvancados() {
|
||||
filtrosUi.value = []
|
||||
filtrosFormula.value = ""
|
||||
limparFiltroAvancado(props.tabela.nome)
|
||||
modalFiltroAberto.value = false
|
||||
valorBusca.value = ""
|
||||
if (paginaAtual.value !== 1) paginaAtual.value = 1
|
||||
else void carregar()
|
||||
}
|
||||
|
||||
function salvarFiltrosAvancados(novoLinhas: LinhaFiltroUI[], novaFormula?: string) {
|
||||
filtrosUi.value = novoLinhas ?? []
|
||||
filtrosFormula.value = novaFormula ?? ""
|
||||
salvarFiltroAvancado<Record<string, unknown>>(props.tabela.nome, {
|
||||
linhas: filtrosUi.value,
|
||||
formula: filtrosFormula.value,
|
||||
})
|
||||
modalFiltroAberto.value = false
|
||||
valorBusca.value = ""
|
||||
if (paginaAtual.value !== 1) paginaAtual.value = 1
|
||||
else void carregar()
|
||||
}
|
||||
|
||||
return {
|
||||
filtrosUi,
|
||||
filtrosFormula,
|
||||
modalFiltroAberto,
|
||||
exibirFiltroAvancado,
|
||||
filtrosAvancadosAtivos,
|
||||
abrirModalFiltro,
|
||||
fecharModalFiltro,
|
||||
limparFiltrosAvancados,
|
||||
salvarFiltrosAvancados,
|
||||
}
|
||||
}
|
||||
91
src/componentes/EliTabela/useEliTabelaMenu.ts
Normal file
91
src/componentes/EliTabela/useEliTabelaMenu.ts
Normal file
|
|
@ -0,0 +1,91 @@
|
|||
/**
|
||||
* useEliTabelaMenu
|
||||
* Gerencia o menu de ações por linha da EliTabela.
|
||||
* Controla abertura/fechamento, posicionamento do popup e detecção de clique externo.
|
||||
*
|
||||
* @param props - Props do componente (tabela.acoesLinha)
|
||||
* @param acoesVisiveis - Ref com matriz de visibilidade das ações por linha
|
||||
*/
|
||||
import { nextTick, onBeforeUnmount, onMounted, type Ref, ref } from "vue"
|
||||
import type { tipoEliTabelaAcao } from "./types-eli-tabela"
|
||||
|
||||
// biome-ignore lint/suspicious/noExplicitAny: genérico de linha passado por prop
|
||||
type PropsMenu = { tabela: { acoesLinha?: tipoEliTabelaAcao<any>[] } }
|
||||
|
||||
export function useEliTabelaMenu(props: PropsMenu, acoesVisiveis: Ref<boolean[][]>) {
|
||||
const menuAberto = ref<number | null>(null)
|
||||
const menuPopup = ref<{ menuEl: HTMLElement | null } | null>(null)
|
||||
const menuPopupPos = ref({ top: 0, left: 0 })
|
||||
|
||||
function atualizarPosicaoMenu(anchor: HTMLElement) {
|
||||
const rect = anchor.getBoundingClientRect()
|
||||
const gap = 8
|
||||
const alturaMenu = menuPopup.value?.menuEl?.offsetHeight ?? 0
|
||||
const larguraMenu = menuPopup.value?.menuEl?.offsetWidth ?? 180
|
||||
let top = rect.bottom + gap
|
||||
const left = rect.right - larguraMenu
|
||||
if (alturaMenu && top + alturaMenu > window.innerHeight - gap) {
|
||||
top = rect.top - gap - alturaMenu
|
||||
}
|
||||
menuPopupPos.value = {
|
||||
top: Math.max(gap, Math.round(top)),
|
||||
left: Math.max(gap, Math.round(left)),
|
||||
}
|
||||
}
|
||||
|
||||
function handleClickFora(evento: MouseEvent) {
|
||||
if (menuAberto.value === null) return
|
||||
const alvo = evento.target as Node
|
||||
if (menuPopup.value?.menuEl?.contains(alvo)) return
|
||||
if (import.meta.env.DEV) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log("[EliTabela] click fora => fechar menu", { menuAberto: menuAberto.value })
|
||||
}
|
||||
menuAberto.value = null
|
||||
}
|
||||
|
||||
function acoesDisponiveisPorLinha(i: number) {
|
||||
const acoesLinha = props.tabela.acoesLinha ?? []
|
||||
const visibilidade = acoesVisiveis.value[i] ?? []
|
||||
return acoesLinha
|
||||
.map((acao, indice) => {
|
||||
const fallbackVisivel =
|
||||
acao.exibir === undefined ? true : typeof acao.exibir === "boolean" ? acao.exibir : false
|
||||
return { acao, indice, visivel: visibilidade[indice] ?? fallbackVisivel }
|
||||
})
|
||||
.filter((item) => item.visivel)
|
||||
}
|
||||
|
||||
function possuiAcoes(i: number) {
|
||||
return acoesDisponiveisPorLinha(i).length > 0
|
||||
}
|
||||
|
||||
function toggleMenu(i: number, evento?: MouseEvent) {
|
||||
if (!possuiAcoes(i)) return
|
||||
if (import.meta.env.DEV) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log("[EliTabela] toggleMenu (antes)", { i, atual: menuAberto.value })
|
||||
}
|
||||
if (menuAberto.value === i) {
|
||||
menuAberto.value = null
|
||||
return
|
||||
}
|
||||
menuAberto.value = i
|
||||
if (import.meta.env.DEV) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log("[EliTabela] toggleMenu => abriu", { i })
|
||||
}
|
||||
const anchor = (evento?.currentTarget as HTMLElement | null) ?? null
|
||||
if (anchor) {
|
||||
atualizarPosicaoMenu(anchor)
|
||||
nextTick(() => {
|
||||
if (menuAberto.value !== null) atualizarPosicaoMenu(anchor)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => document.addEventListener("click", handleClickFora))
|
||||
onBeforeUnmount(() => document.removeEventListener("click", handleClickFora))
|
||||
|
||||
return { menuAberto, menuPopup, menuPopupPos, possuiAcoes, toggleMenu, acoesDisponiveisPorLinha }
|
||||
}
|
||||
96
src/componentes/EliTabela/useEliTabelaSelecao.ts
Normal file
96
src/componentes/EliTabela/useEliTabelaSelecao.ts
Normal file
|
|
@ -0,0 +1,96 @@
|
|||
/**
|
||||
* useEliTabelaSelecao
|
||||
* Gerencia a seleção múltipla de linhas na EliTabela.
|
||||
* Suporta seleção por objeto completo ou por chave configurável (`chave_selecao`).
|
||||
*
|
||||
* @param props - Props do componente (selecionados, tabela.chave_selecao, tabela.mostrar_selecao)
|
||||
* @param emit - Função de emit para `update:selecionados`
|
||||
* @param linhasPaginadas - Ref com as linhas da página atual (para todosSelecionados/algunsSelecionados)
|
||||
*/
|
||||
import { computed, type Ref } from "vue"
|
||||
|
||||
type PropsSelecao = {
|
||||
selecionados: unknown[]
|
||||
tabela: { mostrar_selecao?: boolean; chave_selecao?: string | number | symbol }
|
||||
}
|
||||
|
||||
export function useEliTabelaSelecao(
|
||||
props: PropsSelecao,
|
||||
emit: (event: "update:selecionados", value: unknown[]) => void,
|
||||
linhasPaginadas: Ref<unknown[]>,
|
||||
) {
|
||||
const mostrarSelecao = computed(() => Boolean(props.tabela.mostrar_selecao))
|
||||
const chaveSelecao = computed(() => props.tabela.chave_selecao as string | undefined)
|
||||
|
||||
function obterChaveLinha(linha: unknown): unknown {
|
||||
if (chaveSelecao.value && linha && typeof linha === "object") {
|
||||
return (linha as Record<string, unknown>)[chaveSelecao.value]
|
||||
}
|
||||
return linha
|
||||
}
|
||||
|
||||
function isSelecionado(linha: unknown): boolean {
|
||||
const chave = obterChaveLinha(linha)
|
||||
if (chaveSelecao.value) {
|
||||
return props.selecionados.some((s) => obterChaveLinha(s) === chave)
|
||||
}
|
||||
return props.selecionados.includes(linha)
|
||||
}
|
||||
|
||||
function alternarSelecao(linha: unknown) {
|
||||
const atual = [...props.selecionados]
|
||||
const chave = obterChaveLinha(linha)
|
||||
|
||||
let index = -1
|
||||
if (chaveSelecao.value) {
|
||||
index = atual.findIndex((s) => obterChaveLinha(s) === chave)
|
||||
} else {
|
||||
index = atual.indexOf(linha)
|
||||
}
|
||||
|
||||
if (index === -1) {
|
||||
atual.push(linha)
|
||||
} else {
|
||||
atual.splice(index, 1)
|
||||
}
|
||||
emit("update:selecionados", atual)
|
||||
}
|
||||
|
||||
const todosSelecionados = computed(() => {
|
||||
if (linhasPaginadas.value.length === 0) return false
|
||||
return linhasPaginadas.value.every((linha) => isSelecionado(linha))
|
||||
})
|
||||
|
||||
const algunsSelecionados = computed(() => {
|
||||
if (linhasPaginadas.value.length === 0) return false
|
||||
return linhasPaginadas.value.some((linha) => isSelecionado(linha)) && !todosSelecionados.value
|
||||
})
|
||||
|
||||
function alternarSelecaoTodos(forcarMarcar?: boolean) {
|
||||
const marcarTodos = forcarMarcar ?? !todosSelecionados.value
|
||||
let atual = [...props.selecionados]
|
||||
|
||||
if (marcarTodos) {
|
||||
for (const linha of linhasPaginadas.value) {
|
||||
if (!isSelecionado(linha)) atual.push(linha)
|
||||
}
|
||||
} else {
|
||||
const chavesPagina = linhasPaginadas.value.map(obterChaveLinha)
|
||||
if (chaveSelecao.value) {
|
||||
atual = atual.filter((s) => !chavesPagina.includes(obterChaveLinha(s)))
|
||||
} else {
|
||||
atual = atual.filter((s) => !chavesPagina.includes(s))
|
||||
}
|
||||
}
|
||||
emit("update:selecionados", atual)
|
||||
}
|
||||
|
||||
return {
|
||||
mostrarSelecao,
|
||||
isSelecionado,
|
||||
alternarSelecao,
|
||||
todosSelecionados,
|
||||
algunsSelecionados,
|
||||
alternarSelecaoTodos,
|
||||
}
|
||||
}
|
||||
211
src/componentes/EliTabela/useFiltroAvancado.ts
Normal file
211
src/componentes/EliTabela/useFiltroAvancado.ts
Normal file
|
|
@ -0,0 +1,211 @@
|
|||
/**
|
||||
* useFiltroAvancado
|
||||
* Composable que encapsula a lógica do modal de filtro avançado da EliTabela.
|
||||
*
|
||||
* Responsável por:
|
||||
* - Gerenciar a lista de linhas de filtro ativas
|
||||
* - Converter fórmulas booleanas entre notação de exibição (`{c1}`) e persistência (`{chave}`)
|
||||
* - Normalizar filtros carregados do localStorage (compatibilidade com formatos legados)
|
||||
* - Resolver o componente de entrada correto por tipo (`texto`, `numero`, `dataHora`)
|
||||
*
|
||||
* Extraído de `EliTabelaModalFiltroAvancado.vue` para manter o componente abaixo de 250 linhas.
|
||||
*/
|
||||
import { computed, type Ref, ref, watch } from "vue"
|
||||
import { EliEntradaDataHora, EliEntradaNumero, EliEntradaTexto } from "../EliEntrada"
|
||||
import type { ComponenteEntrada } from "../EliEntrada/tiposEntradas"
|
||||
import type { tipoEliTabelaConsulta } from "./types-eli-tabela"
|
||||
|
||||
export type FiltroBase<T> = NonNullable<tipoEliTabelaConsulta<T>["filtroAvancado"]>[number]
|
||||
|
||||
export type LinhaFiltro = {
|
||||
chave: string
|
||||
entrada: ComponenteEntrada
|
||||
/** Valor do campo de filtro — unknown intencionalmente (vem de localStorage ou entrada do usuário). */
|
||||
valor: unknown
|
||||
}
|
||||
|
||||
/**
|
||||
* Retorna o rótulo de exibição de uma definição de filtro.
|
||||
* Utilizado no template do modal para exibir o nome do campo ao usuário.
|
||||
*
|
||||
* @param f - Definição de filtro da configuração da tabela
|
||||
* @returns Rótulo textual do filtro
|
||||
*/
|
||||
export function rotuloDoFiltro(f: FiltroBase<string>) {
|
||||
return f.rotulo
|
||||
}
|
||||
|
||||
interface UseFiltroAvancadoOptions {
|
||||
aberto: Ref<boolean>
|
||||
filtrosBase: Ref<FiltroBase<string>[]>
|
||||
modelo: Ref<Array<{ chave: string; valor: unknown }>>
|
||||
modeloFormula: Ref<string>
|
||||
emit: {
|
||||
(event: "fechar"): void
|
||||
(event: "limpar"): void
|
||||
(event: "salvar", linhas: Array<{ chave: string; valor: unknown }>, formula?: string): void
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Composable principal do modal de filtro avançado da EliTabela.
|
||||
* Sincroniza automaticamente quando o modal abre ou o modelo/filtrosBase mudam.
|
||||
*
|
||||
* @param options.aberto - Ref que controla se o modal está aberto
|
||||
* @param options.filtrosBase - Ref com as definições de filtros disponíveis na tabela
|
||||
* @param options.modelo - Ref com as linhas de filtro persistidas (vindo do pai)
|
||||
* @param options.modeloFormula - Ref com a fórmula booleana persistida no pai
|
||||
* @param options.emit - Função de emit do componente Vue pai
|
||||
* @returns Estado reativo e ações do modal de filtro
|
||||
*/
|
||||
export function useFiltroAvancado({
|
||||
aberto,
|
||||
filtrosBase,
|
||||
modelo,
|
||||
modeloFormula,
|
||||
emit,
|
||||
}: UseFiltroAvancadoOptions) {
|
||||
const linhas = ref<LinhaFiltro[]>([])
|
||||
const chaveParaAdicionar = ref<string>("")
|
||||
const formulaCustomizada = ref("")
|
||||
|
||||
const formulaGerada = computed(() => linhas.value.map((_, i) => `{c${i + 1}}`).join(" E "))
|
||||
|
||||
function formulaParaExibicao(formula: string): string {
|
||||
let resultado = formula
|
||||
linhas.value.forEach((l, i) => {
|
||||
resultado = resultado.replace(new RegExp(`\\{${l.chave}\\}`, "g"), `{c${i + 1}}`)
|
||||
})
|
||||
return resultado
|
||||
}
|
||||
|
||||
function formulaParaPersistencia(formula: string): string {
|
||||
let resultado = formula
|
||||
for (let i = linhas.value.length; i >= 1; i--) {
|
||||
const chave = linhas.value[i - 1]?.chave
|
||||
if (chave) {
|
||||
resultado = resultado.replace(new RegExp(`\\{c${i}\\}`, "g"), `{${chave}}`)
|
||||
}
|
||||
}
|
||||
return resultado
|
||||
}
|
||||
|
||||
const chavesDisponiveis = computed(() => (filtrosBase.value ?? []).map((b) => String(b.chave)))
|
||||
|
||||
const opcoesParaAdicionar = computed(() => {
|
||||
const usadas = new Set(linhas.value.map((l) => String(l.chave)))
|
||||
return (filtrosBase.value ?? []).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) {
|
||||
const opcoes: Record<string, unknown> =
|
||||
typeof entrada?.[1] === "object" && entrada[1] !== null
|
||||
? { ...(entrada[1] as Record<string, unknown>) }
|
||||
: {}
|
||||
return { ...opcoes, rotulo: "" }
|
||||
}
|
||||
|
||||
function valorInicialPorEntrada(entrada: ComponenteEntrada) {
|
||||
if (entrada?.[0] === "numero") return null
|
||||
return ""
|
||||
}
|
||||
|
||||
function normalizarModelo() {
|
||||
const base = filtrosBase.value ?? []
|
||||
const modeloArr = Array.isArray(modelo.value) ? modelo.value : []
|
||||
linhas.value = modeloArr
|
||||
.map((m: unknown) => {
|
||||
if (!m || typeof m !== "object") return null
|
||||
const item = m as Record<string, unknown>
|
||||
const baseItem = base.find((b) => String(b.chave) === String(item["chave"])) ?? base[0]
|
||||
if (!baseItem) return null
|
||||
const entrada = (baseItem.entrada ?? item["entrada"]) as ComponenteEntrada
|
||||
const chave = String(baseItem.chave ?? item["chave"])
|
||||
const val = item["valor"] ?? valorInicialPorEntrada(entrada)
|
||||
return { chave, entrada, valor: val } as LinhaFiltro
|
||||
})
|
||||
.filter(Boolean) as LinhaFiltro[]
|
||||
|
||||
linhas.value = linhas.value.filter((l) => chavesDisponiveis.value.includes(String(l.chave)))
|
||||
|
||||
if (modeloFormula.value) {
|
||||
formulaCustomizada.value = formulaParaExibicao(modeloFormula.value)
|
||||
} else {
|
||||
formulaCustomizada.value = formulaGerada.value
|
||||
}
|
||||
}
|
||||
|
||||
watch(
|
||||
() => [aberto.value, filtrosBase.value, modelo.value] as const,
|
||||
() => {
|
||||
if (aberto.value) normalizarModelo()
|
||||
},
|
||||
{ deep: true, immediate: true },
|
||||
)
|
||||
|
||||
function adicionar() {
|
||||
if (!chaveParaAdicionar.value) return
|
||||
const b0 = (filtrosBase.value ?? []).find(
|
||||
(b) => String(b.chave) === String(chaveParaAdicionar.value),
|
||||
)
|
||||
if (!b0) return
|
||||
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),
|
||||
})
|
||||
formulaCustomizada.value = formulaGerada.value
|
||||
chaveParaAdicionar.value = ""
|
||||
}
|
||||
|
||||
function remover(idx: number) {
|
||||
linhas.value.splice(idx, 1)
|
||||
formulaCustomizada.value = formulaGerada.value
|
||||
}
|
||||
|
||||
function emitFechar() {
|
||||
emit("fechar")
|
||||
}
|
||||
function emitLimpar() {
|
||||
emit("limpar")
|
||||
}
|
||||
|
||||
function emitSalvar() {
|
||||
const formulaPersistida = formulaParaPersistencia(formulaCustomizada.value)
|
||||
emit(
|
||||
"salvar",
|
||||
linhas.value.map((l) => ({ chave: l.chave, valor: l.valor })),
|
||||
formulaPersistida,
|
||||
)
|
||||
}
|
||||
|
||||
function rotuloDoFiltroAtivo(chave: string): string {
|
||||
const base = (filtrosBase.value ?? []).find((b) => String(b.chave) === chave)
|
||||
return base ? rotuloDoFiltro(base) : chave
|
||||
}
|
||||
|
||||
return {
|
||||
linhas,
|
||||
opcoesParaAdicionar,
|
||||
chaveParaAdicionar,
|
||||
formulaCustomizada,
|
||||
formulaGerada,
|
||||
componenteEntrada,
|
||||
opcoesEntrada,
|
||||
adicionar,
|
||||
remover,
|
||||
emitFechar,
|
||||
emitSalvar,
|
||||
emitLimpar,
|
||||
rotuloDoFiltro,
|
||||
rotuloDoFiltroAtivo,
|
||||
}
|
||||
}
|
||||
46
src/design-tokens.css
Normal file
46
src/design-tokens.css
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
/**
|
||||
* Design Tokens — Paleta Institucional Azteca
|
||||
*
|
||||
* Arquivo centralizado de variáveis CSS usado por todos os componentes Eli.
|
||||
* Importado no ponto de entrada do projeto (main.ts ou App.vue).
|
||||
*
|
||||
* Escala primária: Verde floresta (#688C00)
|
||||
* Escala de apoio: Âmbar terroso
|
||||
* Escala neutra: Cinza musgo
|
||||
*/
|
||||
|
||||
:root {
|
||||
/* ── Escala primária — Verde floresta ── */
|
||||
--cor-eli-primaria-bg: #EEF3D0;
|
||||
--cor-eli-primaria-zebra: #F6F9E8;
|
||||
--cor-eli-primaria-suave: #BFC99C;
|
||||
--cor-eli-primaria-header: #ADCC44;
|
||||
--cor-eli-primaria-clara: #D4E290;
|
||||
--cor-eli-primaria-media: #ADCC44;
|
||||
--cor-eli-primaria-acao: #8AAF00;
|
||||
--cor-eli-primaria: #688C00;
|
||||
--cor-eli-primaria-escura: #4A6500;
|
||||
--cor-eli-primaria-texto: #2E3F00;
|
||||
|
||||
/* ── Escala de apoio — Âmbar terroso ── */
|
||||
--cor-eli-alerta-bg: #FAF3DF;
|
||||
--cor-eli-alerta-clara: #F5DC8A;
|
||||
--cor-eli-alerta: #DDB847;
|
||||
--cor-eli-alerta-forte: #8C6A00;
|
||||
|
||||
/* ── Escala neutra — Cinza musgo ── */
|
||||
--cor-eli-fundo: #F2F3EE;
|
||||
--cor-eli-borda: #D5D8CC;
|
||||
--cor-eli-borda-forte: #B5BAA8;
|
||||
--cor-eli-neutro: #5E6553;
|
||||
--cor-eli-neutro-escuro: #3E4436;
|
||||
--cor-eli-superficie: #FFFFFF;
|
||||
|
||||
/* ── Semânticas ── */
|
||||
--cor-eli-sucesso: #2E7D32;
|
||||
--cor-eli-sucesso-bg: #E8F5E9;
|
||||
--cor-eli-erro: #C62828;
|
||||
--cor-eli-erro-bg: #FFEBEE;
|
||||
--cor-eli-info: #1565C0;
|
||||
--cor-eli-info-bg: #E3F2FD;
|
||||
}
|
||||
|
|
@ -1,5 +1,6 @@
|
|||
import type { App, Plugin } from "vue"
|
||||
import "./styles/eli-vue-fonts.css"
|
||||
import "./design-tokens.css"
|
||||
import { EliBotao } from "./componentes/botao"
|
||||
import { EliCartao } from "./componentes/cartao"
|
||||
import {
|
||||
|
|
@ -13,7 +14,7 @@ import { EliTabela } from "./componentes/EliTabela"
|
|||
import { EliBadge } from "./componentes/indicador"
|
||||
import { EliOlaMundo } from "./componentes/ola_mundo"
|
||||
|
||||
export { criarFiltro26 } from "p-comuns"
|
||||
export { iniciarFiltroDelta } from "p-comuns"
|
||||
export * from "./componentes/EliEntrada"
|
||||
// Exportar tudo (componentes + types + helpers) de Tabela e Entrada
|
||||
export * from "./componentes/EliTabela"
|
||||
|
|
|
|||
|
|
@ -1,33 +1,78 @@
|
|||
<template>
|
||||
<v-container class="py-6">
|
||||
<h1 class="text-h5 mb-4">Playground — eli-vue</h1>
|
||||
<div style="display: flex; flex-direction: row; min-height: 100vh;">
|
||||
<!-- Seção de Componentes -->
|
||||
<div style="flex: 1; min-width: 0; overflow-y: auto;">
|
||||
<v-container class="py-6">
|
||||
<h1 class="text-h5 mb-4">Playground — eli-vue</h1>
|
||||
|
||||
<v-tabs v-model="aba" color="primary" density="comfortable">
|
||||
<v-tab value="botao">Botão</v-tab>
|
||||
<v-tab value="indicador">Indicador</v-tab>
|
||||
<v-tab value="cartao">Cartão</v-tab>
|
||||
<v-tab value="entradas">Entradas</v-tab>
|
||||
<v-tab value="data_hora">Data e hora</v-tab>
|
||||
<v-tab value="tabela">Tabela</v-tab>
|
||||
<v-tab value="ola_mundo">Demo</v-tab>
|
||||
</v-tabs>
|
||||
|
||||
<v-divider class="my-4" />
|
||||
|
||||
<v-tabs v-model="aba" color="primary" density="comfortable">
|
||||
<v-tab value="botao">Botão</v-tab>
|
||||
<v-tab value="indicador">Indicador</v-tab>
|
||||
<v-tab value="cartao">Cartão</v-tab>
|
||||
<v-tab value="entradas">Entradas</v-tab>
|
||||
<v-tab value="data_hora">Data e hora</v-tab>
|
||||
<v-tab value="tabela">Tabela</v-tab>
|
||||
<v-tab value="ola_mundo">Demo</v-tab>
|
||||
</v-tabs>
|
||||
<BotaoPlayground v-if="aba === 'botao'" />
|
||||
<IndicadorPlayground v-else-if="aba === 'indicador'" />
|
||||
<CartaoPlayground v-else-if="aba === 'cartao'" />
|
||||
<EntradasPlayground v-else-if="aba === 'entradas'" />
|
||||
<DataHoraPlayground v-else-if="aba === 'data_hora'" />
|
||||
<TabelaPlayground v-else-if="aba === 'tabela'" />
|
||||
<OlaMundoPlayground v-else />
|
||||
</v-container>
|
||||
</div>
|
||||
|
||||
<v-divider class="my-4" />
|
||||
<!-- Console Global do Playground -->
|
||||
<div
|
||||
class="playground-console"
|
||||
style="width: 400px; flex-shrink: 0; background: #121212; color: #a3e635; padding: 24px 20px; border-left: 1px solid #333; display: flex; flex-direction: column; font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, monospace; font-size: 12px;"
|
||||
>
|
||||
<div style="display: flex; justify-content: space-between; align-items: center; border-bottom: 1px solid #333; padding-bottom: 12px; margin-bottom: 16px;">
|
||||
<span style="font-weight: 700; color: #fff; text-transform: uppercase; letter-spacing: 1.5px; font-size: 11px; display: flex; align-items: center; gap: 8px;">
|
||||
Console
|
||||
<div style="width: 6px; height: 6px; border-radius: 50%; background-color: #22c55e; box-shadow: 0 0 8px #22c55e;"></div>
|
||||
</span>
|
||||
<button
|
||||
@click="logsConsole = []"
|
||||
style="font-size: 10px; cursor: pointer; color: #ef4444; background: rgba(239, 68, 68, 0.1); border: 1px solid rgba(239, 68, 68, 0.2); border-radius: 4px; padding: 2px 8px; font-family: inherit;"
|
||||
>LIMPAR</button>
|
||||
</div>
|
||||
|
||||
<BotaoPlayground v-if="aba === 'botao'" />
|
||||
<IndicadorPlayground v-else-if="aba === 'indicador'" />
|
||||
<CartaoPlayground v-else-if="aba === 'cartao'" />
|
||||
<EntradasPlayground v-else-if="aba === 'entradas'" />
|
||||
<DataHoraPlayground v-else-if="aba === 'data_hora'" />
|
||||
<TabelaPlayground v-else-if="aba === 'tabela'" />
|
||||
<OlaMundoPlayground v-else />
|
||||
</v-container>
|
||||
<div style="flex: 1; overflow-y: auto; display: flex; flex-direction: column; gap: 8px;" class="playground-console-scroll">
|
||||
<div v-if="logsConsole.length === 0" style="color: #52525b; font-size: 11px;">Nenhum log registrado ainda...</div>
|
||||
<pre
|
||||
v-for="(log, i) in logsConsole"
|
||||
:key="i"
|
||||
style="font-size: 11px; white-space: pre-wrap; word-break: break-all; color: #a1a1aa; border-left: 2px solid #52525b; padding-left: 8px; margin: 0;"
|
||||
>{{ log }}</pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.playground-console-scroll::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
}
|
||||
.playground-console-scroll::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
.playground-console-scroll::-webkit-scrollbar-thumb {
|
||||
background: #3f3f46;
|
||||
border-radius: 3px;
|
||||
}
|
||||
.playground-console-scroll::-webkit-scrollbar-thumb:hover {
|
||||
background: #52525b;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from "vue"
|
||||
import { defineComponent, provide, ref } from "vue"
|
||||
import BotaoPlayground from "./botao.playground.vue"
|
||||
import CartaoPlayground from "./cartao.playground.vue"
|
||||
import DataHoraPlayground from "./data_hora.playground.vue"
|
||||
|
|
@ -65,6 +110,15 @@ const mapaAbaParaHash: Record<AbaPlayground, string> = {
|
|||
ola_mundo: "demo",
|
||||
}
|
||||
|
||||
/** Chave de injeção para o console do playground */
|
||||
export const PLAYGROUND_CONSOLE_KEY = "playgroundConsole"
|
||||
|
||||
/** Tipagem do console injetado */
|
||||
export interface PlaygroundConsole {
|
||||
adicionarLog: (acao: string, req: unknown, res: unknown) => void
|
||||
logMensagem: (mensagem: string) => void
|
||||
}
|
||||
|
||||
export default defineComponent({
|
||||
name: "App",
|
||||
components: {
|
||||
|
|
@ -76,43 +130,34 @@ export default defineComponent({
|
|||
TabelaPlayground,
|
||||
OlaMundoPlayground,
|
||||
},
|
||||
data() {
|
||||
setup() {
|
||||
const logsConsole = ref<string[]>([])
|
||||
|
||||
/** Adiciona um log estruturado ao console global do playground */
|
||||
function adicionarLog(acao: string, req: unknown, res: unknown) {
|
||||
const time = new Date().toLocaleTimeString()
|
||||
logsConsole.value.unshift(
|
||||
`[${time}] 🌐 ${acao}\nReq: ${JSON.stringify(req, null, 2)}\nRes: ${JSON.stringify(res, null, 2)}`,
|
||||
)
|
||||
}
|
||||
|
||||
/** Adiciona uma mensagem simples ao console */
|
||||
function logMensagem(mensagem: string) {
|
||||
const time = new Date().toLocaleTimeString()
|
||||
logsConsole.value.unshift(`[${time}] ${mensagem}`)
|
||||
}
|
||||
|
||||
const consoleApi: PlaygroundConsole = { adicionarLog, logMensagem }
|
||||
provide(PLAYGROUND_CONSOLE_KEY, consoleApi)
|
||||
|
||||
return {
|
||||
aba: "botao" as AbaPlayground,
|
||||
fontVars: {
|
||||
eli: "",
|
||||
v: "",
|
||||
body: "",
|
||||
},
|
||||
googleSansCheck: "(checando...)" as string,
|
||||
logsConsole,
|
||||
aba: ref<AbaPlayground>("botao"),
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.sincronizarAbaComHash()
|
||||
window.addEventListener("hashchange", this.sincronizarAbaComHash)
|
||||
|
||||
// Coleta variáveis de fonte e confirma carregamento via FontFaceSet.
|
||||
this.fontVars.eli = getComputedStyle(document.documentElement)
|
||||
.getPropertyValue("--eli-font-family")
|
||||
.trim()
|
||||
this.fontVars.v = getComputedStyle(document.documentElement)
|
||||
.getPropertyValue("--v-font-family")
|
||||
.trim()
|
||||
this.fontVars.body = getComputedStyle(document.body).fontFamily
|
||||
|
||||
// Espera fonts carregarem e então checa se o navegador considera Google Sans disponível
|
||||
// (isso não garante 100% que TODOS pesos/estilos foram baixados, mas confirma que a face existe/foi carregada)
|
||||
document.fonts.ready
|
||||
.then(() => {
|
||||
this.googleSansCheck = String(
|
||||
document.fonts.check('12px "Google Sans"'),
|
||||
)
|
||||
// atualiza body depois do load, pois pode mudar após o font-face carregar
|
||||
this.fontVars.body = getComputedStyle(document.body).fontFamily
|
||||
})
|
||||
.catch(() => {
|
||||
this.googleSansCheck = "erro"
|
||||
})
|
||||
},
|
||||
beforeUnmount() {
|
||||
window.removeEventListener("hashchange", this.sincronizarAbaComHash)
|
||||
|
|
@ -129,19 +174,14 @@ export default defineComponent({
|
|||
},
|
||||
sincronizarAbaComHash() {
|
||||
const hash = this.normalizarHash()
|
||||
|
||||
const aba = mapaHashParaAba[hash] ?? "botao"
|
||||
|
||||
if (this.aba !== aba) {
|
||||
this.aba = aba
|
||||
}
|
||||
|
||||
// Se a URL estiver vazia/fora do padrão, normalizamos.
|
||||
this.sincronizarHashComAba()
|
||||
},
|
||||
sincronizarHashComAba() {
|
||||
const destino = `#/${mapaAbaParaHash[this.aba]}`
|
||||
|
||||
if (window.location.hash !== destino) {
|
||||
window.location.hash = destino
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,445 +0,0 @@
|
|||
import { respostaComuns, type tipoResposta } from "p-respostas"
|
||||
import type { tipoFiltro } from "@/componentes/EliTabela/types-eli-tabela"
|
||||
|
||||
export type Linha = {
|
||||
codigo: string
|
||||
empreendedor: string
|
||||
empreendimento: string
|
||||
documento: string
|
||||
email: string
|
||||
telefone: string
|
||||
faturamento: number
|
||||
peso_kg: number
|
||||
criado_em: string
|
||||
atualizado_em: string
|
||||
}
|
||||
|
||||
// == SIMULAÇÃO DE API (CRUD) ==
|
||||
// Dados isolados (simulando um banco de dados no servidor)
|
||||
const _bancoDeDados: Linha[] = [
|
||||
{
|
||||
codigo: String(Math.random()),
|
||||
empreendedor: "Maria Silva",
|
||||
empreendimento: "Doces da Maria",
|
||||
documento: "12.345.678/0001-90",
|
||||
email:
|
||||
"contato.comercial.super.longo@doces-da-maria-exemplo-muito-grande.com.br",
|
||||
telefone: "(11) 91234-5678",
|
||||
faturamento: 12500.5,
|
||||
peso_kg: 12.4,
|
||||
criado_em: "2026-01-09T16:15:00Z",
|
||||
atualizado_em: "2026-01-29T15:30:00-03:00",
|
||||
},
|
||||
{
|
||||
codigo: String(Math.random()),
|
||||
empreendedor: "João Pereira",
|
||||
empreendimento: "Café Peregrino",
|
||||
documento: "98.765.432/0001-12",
|
||||
email: "contato@cafeperegrino.com",
|
||||
telefone: "(11) 93456-7810",
|
||||
faturamento: 8000,
|
||||
peso_kg: 6.2,
|
||||
criado_em: "2026-01-02T10:00:00-03:00",
|
||||
atualizado_em: "2026-01-20T08:10:00-03:00",
|
||||
},
|
||||
{
|
||||
codigo: String(Math.random()),
|
||||
empreendedor: "Ana Costa",
|
||||
empreendimento: "Flor de Sal Gastronomia",
|
||||
documento: "45.678.912/0001-55",
|
||||
email: "ana@flordesal.com",
|
||||
telefone: "(21) 99876-5432",
|
||||
faturamento: 3200.75,
|
||||
peso_kg: 4.8,
|
||||
criado_em: "2025-12-10T09:30:00-03:00",
|
||||
atualizado_em: "2026-01-15T19:20:00-03:00",
|
||||
},
|
||||
{
|
||||
codigo: String(Math.random()),
|
||||
empreendedor: "Rafael Lima",
|
||||
empreendimento: "Tech Agro Solutions",
|
||||
documento: "07.654.321/0001-00",
|
||||
email: "rafael@techagro.com",
|
||||
telefone: "(31) 94567-8899",
|
||||
faturamento: 15999.9,
|
||||
peso_kg: 9.1,
|
||||
criado_em: "2026-01-12T12:00:00-03:00",
|
||||
atualizado_em: "2026-01-28T11:00:00-03:00",
|
||||
},
|
||||
{
|
||||
codigo: String(Math.random()),
|
||||
empreendedor: "Juliana Torres",
|
||||
empreendimento: "Torres Moda Sustentável",
|
||||
documento: "33.210.987/0001-44",
|
||||
email: "juliana@torresmoda.com",
|
||||
telefone: "(71) 97766-5544",
|
||||
faturamento: 4500,
|
||||
peso_kg: 3.3,
|
||||
criado_em: "2026-01-01T00:00:00Z",
|
||||
atualizado_em: "2026-01-10T13:45:00-03:00",
|
||||
},
|
||||
{
|
||||
codigo: String(Math.random()),
|
||||
empreendedor: "Marcos Vieira",
|
||||
empreendimento: "Padaria Pão Quentinho",
|
||||
documento: "21.109.876/0001-32",
|
||||
email: "marcos@paoquentinho.com",
|
||||
telefone: "(48) 99654-3210",
|
||||
faturamento: 2200.4,
|
||||
peso_kg: 15.7,
|
||||
criado_em: "2026-01-05T07:25:00-03:00",
|
||||
atualizado_em: "2026-01-23T20:15:00-03:00",
|
||||
},
|
||||
{
|
||||
codigo: String(Math.random()),
|
||||
empreendedor: "Bianca Rocha",
|
||||
empreendimento: "Studio Beleza em Casa",
|
||||
documento: "54.321.098/0001-21",
|
||||
email: "contato@belezaemcasa.com",
|
||||
telefone: "(85) 98877-1122",
|
||||
faturamento: 990,
|
||||
peso_kg: 1.9,
|
||||
criado_em: "2026-01-03T14:00:00-03:00",
|
||||
atualizado_em: "2026-01-29T09:00:00-03:00",
|
||||
},
|
||||
{
|
||||
codigo: String(Math.random()),
|
||||
empreendedor: "Caio Albuquerque",
|
||||
empreendimento: "Albuquerque Engenharia Verde",
|
||||
documento: "65.432.109/0001-09",
|
||||
email: "caio@engenhariaverde.com",
|
||||
telefone: "(61) 98123-4567",
|
||||
faturamento: 100000,
|
||||
peso_kg: 21.3,
|
||||
criado_em: "2025-11-18T16:10:00-03:00",
|
||||
atualizado_em: "2026-01-25T10:40:00-03:00",
|
||||
},
|
||||
{
|
||||
codigo: String(Math.random()),
|
||||
empreendedor: "Fernanda Almeida",
|
||||
empreendimento: "Faz Bem Produtos Naturais",
|
||||
documento: "87.654.210/0001-98",
|
||||
email: "fernanda@fazbem.com",
|
||||
telefone: "(41) 99777-6655",
|
||||
faturamento: 5600.2,
|
||||
peso_kg: 8.6,
|
||||
criado_em: "2026-01-08T12:12:00-03:00",
|
||||
atualizado_em: "2026-01-21T17:55:00-03:00",
|
||||
},
|
||||
{
|
||||
codigo: String(Math.random()),
|
||||
empreendedor: "Gabriel Martins",
|
||||
empreendimento: "Martins Tech Repair",
|
||||
documento: "19.876.543/0001-76",
|
||||
email: "suporte@martinstech.com",
|
||||
telefone: "(19) 98888-9090",
|
||||
faturamento: 7550,
|
||||
peso_kg: 7.7,
|
||||
criado_em: "2026-01-11T11:00:00-03:00",
|
||||
atualizado_em: "2026-01-18T11:30:00-03:00",
|
||||
},
|
||||
{
|
||||
codigo: String(Math.random()),
|
||||
empreendedor: "Helena Duarte",
|
||||
empreendimento: "Ateliê Costura Criativa",
|
||||
documento: "23.456.789/0001-65",
|
||||
email: "helena@costuracriativa.com",
|
||||
telefone: "(51) 98765-4433",
|
||||
faturamento: 2700,
|
||||
peso_kg: 2.5,
|
||||
criado_em: "2026-01-07T08:00:00-03:00",
|
||||
atualizado_em: "2026-01-16T14:10:00-03:00",
|
||||
},
|
||||
{
|
||||
codigo: String(Math.random()),
|
||||
empreendedor: "Igor Santos",
|
||||
empreendimento: "Santos Consultoria Financeira",
|
||||
documento: "43.219.876/0001-54",
|
||||
email: "igor@santosconsultoria.com",
|
||||
telefone: "(31) 99332-1100",
|
||||
faturamento: 12000,
|
||||
peso_kg: 5.4,
|
||||
criado_em: "2026-01-06T10:30:00-03:00",
|
||||
atualizado_em: "2026-01-22T18:30:00-03:00",
|
||||
},
|
||||
{
|
||||
codigo: String(Math.random()),
|
||||
empreendedor: "Jéssica Nunes",
|
||||
empreendimento: "Nunes Eventos & Decoração",
|
||||
documento: "09.876.543/0001-33",
|
||||
email: "jessica@nunesdecora.com",
|
||||
telefone: "(62) 99922-3344",
|
||||
faturamento: 3300.1,
|
||||
peso_kg: 4.1,
|
||||
criado_em: "2026-01-04T13:30:00-03:00",
|
||||
atualizado_em: "2026-01-26T09:20:00-03:00",
|
||||
},
|
||||
{
|
||||
codigo: String(Math.random()),
|
||||
empreendedor: "Leonardo Prado",
|
||||
empreendimento: "Prado Bike Courier",
|
||||
documento: "72.345.098/0001-27",
|
||||
email: "contato@pradobike.com",
|
||||
telefone: "(47) 98444-6677",
|
||||
faturamento: 4100,
|
||||
peso_kg: 6.8,
|
||||
criado_em: "2026-01-02T18:40:00-03:00",
|
||||
atualizado_em: "2026-01-27T12:00:00-03:00",
|
||||
},
|
||||
{
|
||||
codigo: String(Math.random()),
|
||||
empreendedor: "Marina Lopes",
|
||||
empreendimento: "Lopes Design Digital",
|
||||
documento: "56.789.012/0001-11",
|
||||
email: "marina@lopesdesign.com",
|
||||
telefone: "(27) 99911-2233",
|
||||
faturamento: 2100,
|
||||
peso_kg: 3.7,
|
||||
criado_em: "2026-01-09T09:00:00-03:00",
|
||||
atualizado_em: "2026-01-19T15:35:00-03:00",
|
||||
},
|
||||
{
|
||||
codigo: String(Math.random()),
|
||||
empreendedor: "Nicolas Teixeira",
|
||||
empreendimento: "QualiVida Fitness",
|
||||
documento: "34.567.890/0001-02",
|
||||
email: "nicolas@qualivida.com",
|
||||
telefone: "(92) 99456-7788",
|
||||
faturamento: 8600.9,
|
||||
peso_kg: 11.2,
|
||||
criado_em: "2026-01-12T19:15:00-03:00",
|
||||
atualizado_em: "2026-01-29T10:50:00-03:00",
|
||||
},
|
||||
{
|
||||
codigo: String(Math.random()),
|
||||
empreendedor: "Olívia Azevedo",
|
||||
empreendimento: "Azevedo Pet Care",
|
||||
documento: "88.901.234/0001-45",
|
||||
email: "olivia@petcare.com",
|
||||
telefone: "(16) 99788-6655",
|
||||
faturamento: 1900.3,
|
||||
peso_kg: 2.2,
|
||||
criado_em: "2026-01-10T15:00:00-03:00",
|
||||
atualizado_em: "2026-01-14T16:00:00-03:00",
|
||||
},
|
||||
{
|
||||
codigo: String(Math.random()),
|
||||
empreendedor: "Paulo Henrique",
|
||||
empreendimento: "Henrique Soluções Elétricas",
|
||||
documento: "44.556.778/0001-90",
|
||||
email: "paulo@solucoeseletricas.com",
|
||||
telefone: "(13) 98810-2020",
|
||||
faturamento: 50000,
|
||||
peso_kg: 17.9,
|
||||
criado_em: "2026-01-13T10:10:00-03:00",
|
||||
atualizado_em: "2026-01-24T21:00:00-03:00",
|
||||
},
|
||||
{
|
||||
codigo: String(Math.random()),
|
||||
empreendedor: "Renata Souza",
|
||||
empreendimento: "Souza Traduções Especializadas",
|
||||
documento: "11.223.344/0001-08",
|
||||
email: "renata@souzatraducoes.com",
|
||||
telefone: "(24) 99661-7788",
|
||||
faturamento: 6400,
|
||||
peso_kg: 9.9,
|
||||
criado_em: "2026-01-08T07:00:00-03:00",
|
||||
atualizado_em: "2026-01-28T09:05:00-03:00",
|
||||
},
|
||||
{
|
||||
codigo: String(Math.random()),
|
||||
empreendedor: "Tiago Moura",
|
||||
empreendimento: "Moura Agro Sustentável",
|
||||
documento: "78.912.345/0001-67",
|
||||
email: "tiago@mouraagro.com",
|
||||
telefone: "(68) 99999-4545",
|
||||
faturamento: 7200,
|
||||
peso_kg: 13.5,
|
||||
criado_em: "2026-01-03T12:00:00-03:00",
|
||||
atualizado_em: "2026-01-23T12:45:00-03:00",
|
||||
},
|
||||
{
|
||||
codigo: String(Math.random()),
|
||||
empreendedor: "Viviane Castro",
|
||||
empreendimento: "Castro Arte & Cerâmica",
|
||||
documento: "32.165.498/0001-87",
|
||||
email: "viviane@castroarte.com",
|
||||
telefone: "(81) 98787-1212",
|
||||
faturamento: 2800,
|
||||
peso_kg: 4.6,
|
||||
criado_em: "2026-01-01T09:00:00-03:00",
|
||||
atualizado_em: "2026-01-17T09:30:00-03:00",
|
||||
},
|
||||
]
|
||||
|
||||
// Helpers internos da API (não expostos)
|
||||
const _filtrarPorBusca = (lista: Linha[], texto?: string) => {
|
||||
const termo = texto?.trim().toLowerCase()
|
||||
if (!termo) return [...lista]
|
||||
return lista.filter((linha) =>
|
||||
[linha.empreendedor, linha.empreendimento].some((v) =>
|
||||
v.toLowerCase().includes(termo),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
const _comparar = (
|
||||
op: string,
|
||||
valLine: unknown,
|
||||
valFiltro: unknown,
|
||||
): boolean => {
|
||||
switch (op) {
|
||||
case "=":
|
||||
return valLine == valFiltro
|
||||
case "!=":
|
||||
return valLine != valFiltro
|
||||
case ">":
|
||||
return Number(valLine) > Number(valFiltro)
|
||||
case ">=":
|
||||
return Number(valLine) >= Number(valFiltro)
|
||||
case "<":
|
||||
return Number(valLine) < Number(valFiltro)
|
||||
case "<=":
|
||||
return Number(valLine) <= Number(valFiltro)
|
||||
case "like":
|
||||
return String(valLine ?? "")
|
||||
.toLowerCase()
|
||||
.includes(String(valFiltro ?? "").toLowerCase())
|
||||
case "in": {
|
||||
const arr = Array.isArray(valFiltro)
|
||||
? valFiltro
|
||||
: String(valFiltro ?? "")
|
||||
.split(",")
|
||||
.map((s) => s.trim())
|
||||
.filter(Boolean)
|
||||
return arr.includes(String(valLine))
|
||||
}
|
||||
case "isNull":
|
||||
return valLine === null || valLine === undefined || valLine === ""
|
||||
default:
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
const _filtrarAvancado = (lista: Linha[], filtros?: tipoFiltro<Linha>[]) => {
|
||||
if (!filtros?.length) return [...lista]
|
||||
|
||||
return lista.filter((linha) =>
|
||||
filtros.every((f) => {
|
||||
// f é um objeto { [coluna]: { [op]: valor } }
|
||||
// Percorremos as colunas (chaves)
|
||||
return Object.entries(f).every(([coluna, condicoes]) => {
|
||||
// Ignorar chaves de lógica complexa por enquanto no playground
|
||||
if (coluna === "AND" || coluna === "OR") return true
|
||||
|
||||
const valLinha = linha[coluna as keyof Linha]
|
||||
|
||||
// Se condicoes for objeto { op: val }
|
||||
if (typeof condicoes === "object" && condicoes !== null) {
|
||||
return Object.entries(condicoes).every(([op, val]) =>
|
||||
_comparar(op, valLinha, val),
|
||||
)
|
||||
}
|
||||
// Se for valor direto (igualdade implícita? ou erro?)
|
||||
// Vamos assumir igualdade se não for objeto
|
||||
return _comparar("=", valLinha, condicoes)
|
||||
})
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
const _ordenar = (
|
||||
lista: Linha[],
|
||||
p?: { coluna_ordem?: keyof Linha; direcao_ordem?: "asc" | "desc" },
|
||||
) => {
|
||||
if (!p?.coluna_ordem) return [...lista]
|
||||
const dir = p.direcao_ordem === "desc" ? -1 : 1
|
||||
const key = p.coluna_ordem
|
||||
return [...lista].sort((a, b) => {
|
||||
const va = String(a[key] ?? "")
|
||||
const vb = String(b[key] ?? "")
|
||||
return dir * va.localeCompare(vb, "pt-BR", { sensitivity: "base" })
|
||||
})
|
||||
}
|
||||
|
||||
const delay = (ms: number) =>
|
||||
new Promise((r) => setTimeout(r, ms * Math.random() + 2000))
|
||||
|
||||
// -- API PÚBLICA SIMULADA --
|
||||
|
||||
export async function api_ler(params: {
|
||||
texto_busca?: string
|
||||
filtros?: tipoFiltro<Linha>[]
|
||||
coluna_ordem?: keyof Linha
|
||||
direcao_ordem?: "asc" | "desc"
|
||||
limit?: number
|
||||
offSet?: number
|
||||
}) {
|
||||
await delay(600) // simula latência de rede
|
||||
|
||||
// 1. Filtro
|
||||
let resultado = params.texto_busca
|
||||
? _filtrarPorBusca(_bancoDeDados, params.texto_busca)
|
||||
: _filtrarAvancado(_bancoDeDados, params.filtros)
|
||||
|
||||
// 2. Ordenação
|
||||
resultado = _ordenar(resultado, params)
|
||||
|
||||
// 3. Paginação
|
||||
const limit = Math.max(1, Number(params.limit ?? 10))
|
||||
const offset = Math.max(0, Number(params.offSet ?? 0))
|
||||
const paginado = resultado.slice(offset, offset + limit)
|
||||
|
||||
return {
|
||||
total: resultado.length,
|
||||
dados: paginado,
|
||||
}
|
||||
}
|
||||
|
||||
export async function api_criar(
|
||||
dados: Partial<Linha>,
|
||||
): Promise<tipoResposta<Linha>> {
|
||||
await delay(1500) // simula tempo de salvar
|
||||
|
||||
if (!dados.empreendedor) {
|
||||
return respostaComuns.erro("Empreendedor é obrigatório")
|
||||
}
|
||||
|
||||
const novo: Linha = {
|
||||
codigo: Math.random().toString(),
|
||||
empreendedor: dados.empreendedor || "Novo",
|
||||
empreendimento: dados.empreendimento || "Novo Emp",
|
||||
documento: dados.documento || "00.000.000/0000-00",
|
||||
email: dados.email || "novo@email.com",
|
||||
telefone: dados.telefone || "(00) 0000-0000",
|
||||
faturamento: dados.faturamento || 0,
|
||||
peso_kg: dados.peso_kg || 0,
|
||||
criado_em: new Date().toISOString(),
|
||||
atualizado_em: new Date().toISOString(),
|
||||
}
|
||||
_bancoDeDados.unshift(novo)
|
||||
return respostaComuns.valor(novo)
|
||||
}
|
||||
|
||||
export async function api_editar(
|
||||
codigo: string,
|
||||
novosDados: Partial<Linha>,
|
||||
): Promise<tipoResposta<Linha>> {
|
||||
await delay(1000)
|
||||
const idx = _bancoDeDados.find((l) => l.codigo === codigo)
|
||||
if (!idx) return respostaComuns.erro("Registro não encontrado")
|
||||
Object.assign(idx, novosDados)
|
||||
return respostaComuns.valor(idx)
|
||||
}
|
||||
|
||||
export async function api_deletar(
|
||||
codigo: string,
|
||||
): Promise<tipoResposta<string>> {
|
||||
await delay(800)
|
||||
const idx = _bancoDeDados.findIndex((l) => l.codigo === codigo)
|
||||
if (idx !== -1) {
|
||||
_bancoDeDados.splice(idx, 1)
|
||||
return respostaComuns.valor("Registro deletado com sucesso")
|
||||
}
|
||||
return respostaComuns.erro("Registro não encontrado")
|
||||
}
|
||||
195
src/playground/api_simulador/banco.ts
Normal file
195
src/playground/api_simulador/banco.ts
Normal file
|
|
@ -0,0 +1,195 @@
|
|||
import alasql from "alasql"
|
||||
import type { tipoCondicaoFiltroDelta, zFiltroDelta } from "p-comuns"
|
||||
import type z from "zod"
|
||||
import { _dados_empreendedores, type tipoEmpreendedores } from "./dados"
|
||||
|
||||
const zCondicaoFiltroDeltaParaQuery = ({
|
||||
condicao,
|
||||
}: {
|
||||
condicao: tipoCondicaoFiltroDelta
|
||||
}): { sql: string; parametro: Record<string, unknown> } => {
|
||||
const coluna = condicao.coluna
|
||||
|
||||
const retorno: { sql: string; parametro: Record<string, unknown> } = {
|
||||
sql: "",
|
||||
parametro: {},
|
||||
}
|
||||
|
||||
if (condicao.operador === "!=") {
|
||||
retorno.sql = `${coluna} != ${condicao.id}`
|
||||
retorno.parametro[condicao.id] = condicao.valor
|
||||
} else if (condicao.operador === "=") {
|
||||
retorno.sql = `${coluna} = ${condicao.id}`
|
||||
retorno.parametro[condicao.id] = condicao.valor
|
||||
} else if (condicao.operador === ">") {
|
||||
retorno.sql = `${coluna} > ${condicao.id}`
|
||||
retorno.parametro[condicao.id] = condicao.valor
|
||||
} else if (condicao.operador === "<") {
|
||||
retorno.sql = `${coluna} < ${condicao.id}`
|
||||
retorno.parametro[condicao.id] = condicao.valor
|
||||
} else if (condicao.operador === ">=") {
|
||||
retorno.sql = `${coluna} >= ${condicao.id}`
|
||||
retorno.parametro[condicao.id] = condicao.valor
|
||||
} else if (condicao.operador === "<=") {
|
||||
retorno.sql = `${coluna} <= ${condicao.id}`
|
||||
retorno.parametro[condicao.id] = condicao.valor
|
||||
} else if (condicao.operador === "like") {
|
||||
retorno.sql = `LOWER(${coluna}) LIKE LOWER(${condicao.id})`
|
||||
retorno.parametro[condicao.id] = condicao.valor.includes("%")
|
||||
? condicao.valor
|
||||
: `%${condicao.valor}%`
|
||||
} else if (condicao.operador === "in") {
|
||||
// Alasql usa @(?) para arrays. Porém, o passo 2 substitui {id} por ?.
|
||||
// O que resultará em @(?).
|
||||
retorno.sql = `${coluna} IN @(${condicao.id})`
|
||||
retorno.parametro[condicao.id] = Array.isArray(condicao.valor)
|
||||
? condicao.valor
|
||||
: String(condicao.valor)
|
||||
.split(",")
|
||||
.map((s) => s.trim())
|
||||
} else {
|
||||
throw new Error(`Operador inválido: ${condicao.operador as string}`)
|
||||
}
|
||||
|
||||
return retorno
|
||||
}
|
||||
|
||||
/**
|
||||
* Converte um `FiltroDelta` em um fragmento SQL com parâmetros posicionais para o Alasql.
|
||||
* Realiza dois passes na fórmula:
|
||||
* 1. Substitui cada `{id}` pelo sub-SQL da condição correspondente
|
||||
* 2. Extrai os parâmetros em ordem e troca `{id}` por `?`
|
||||
* Por fim, traduz os operadores lógicos pt-BR (E, OU, NAO) para SQL (AND, OR, NOT).
|
||||
*
|
||||
* @param filtros - Um FiltroDelta (fórmula + condições) ou `undefined` para sem filtro
|
||||
* @returns Fragmento SQL e array de parâmetros posicionais
|
||||
*/
|
||||
export const zFiltroDeltaParaQuery = ({
|
||||
filtros,
|
||||
}: {
|
||||
filtros: z.infer<typeof zFiltroDelta> | undefined
|
||||
}): { sql: string; parametros: unknown[] } => {
|
||||
if (!filtros) return { sql: "", parametros: [] }
|
||||
|
||||
const parametros: unknown[] = []
|
||||
const sqlParametros: Record<string, unknown> = {}
|
||||
|
||||
// 1. Substituir `{id}` pelo sub-sql de cada condição e extrair parâmetros
|
||||
let sql = filtros.formula.replace(/\{[\w:]+\}/g, (match) => {
|
||||
const id = match.slice(1, -1)
|
||||
const condicao = filtros.condicoes.find((c) => c.id === match)
|
||||
|
||||
if (!condicao) return match
|
||||
|
||||
const { sql: sqlCondicao, parametro } = zCondicaoFiltroDeltaParaQuery({ condicao })
|
||||
sqlParametros[id] = parametro[match]
|
||||
|
||||
return ` (${sqlCondicao}) `
|
||||
})
|
||||
|
||||
// 2. Extrair parâmetros na ordem correta, trocando os {id} restantes por ?
|
||||
sql = sql.replace(/\{[\w:]+\}/g, (match) => {
|
||||
const id = match.slice(1, -1)
|
||||
parametros.push(sqlParametros[id])
|
||||
return "?"
|
||||
})
|
||||
|
||||
// 3. Traduzir operadores lógicos do filtroDelta
|
||||
sql = sql
|
||||
.replace(/\bNAO\b/g, " NOT ")
|
||||
.replace(/\bE\b/g, " AND ")
|
||||
.replace(/\bOU\b/g, " OR ")
|
||||
|
||||
return { sql, parametros }
|
||||
}
|
||||
|
||||
/**
|
||||
* Combina um array de FiltroDelta em uma cláusula WHERE única para o Alasql.
|
||||
* Cada filtro é convertido individualmente e combinado com AND.
|
||||
* Retorna `"1=1"` (sem filtro) quando o array é vazio.
|
||||
*
|
||||
* @param filtrosArray - Lista de FiltroDelta a combinar; `undefined` ou vazio = sem filtro
|
||||
* @returns Fragmento SQL e array de parâmetros posicionais combinados
|
||||
*/
|
||||
export const zFiltrosArrayParaAlasql = (
|
||||
filtrosArray?: z.infer<typeof zFiltroDelta>[],
|
||||
): { sql: string; parametros: unknown[] } => {
|
||||
if (!filtrosArray?.length) return { sql: "1=1", parametros: [] }
|
||||
|
||||
const queryStrings: string[] = []
|
||||
const todosParametros: unknown[] = []
|
||||
|
||||
for (const filtro of filtrosArray) {
|
||||
const { sql, parametros } = zFiltroDeltaParaQuery({ filtros: filtro })
|
||||
if (sql) {
|
||||
queryStrings.push(`(${sql})`)
|
||||
todosParametros.push(...parametros)
|
||||
}
|
||||
}
|
||||
|
||||
if (queryStrings.length === 0) return { sql: "1=1", parametros: [] }
|
||||
return { sql: queryStrings.join(" AND "), parametros: todosParametros }
|
||||
}
|
||||
|
||||
/**
|
||||
* Executa uma consulta no banco em memória Alasql com suporte a filtros, ordenação e paginação.
|
||||
* Simula o comportamento de uma API REST real para fins de demonstração no playground.
|
||||
* Os filtros são compilados pela `zFiltroDeltaParaQuery`; a contagem total é obtida antes da paginação.
|
||||
*
|
||||
* @param params.filtros - FiltroDelta opcional (filtros avançados do usuário)
|
||||
* @param params.coluna_ordem - Nome da coluna para ordenar
|
||||
* @param params.direcao_ordem - Direção da ordenação (`"asc"` | `"desc"`)
|
||||
* @param params.limit - Número de registros por página
|
||||
* @param params.offSet - Deslocamento (página × limit)
|
||||
* @returns Objeto `{ total, dados }` com o total geral e a página de registros
|
||||
*/
|
||||
export const executarConsultaAlasql = (params: {
|
||||
filtros?: z.infer<typeof zFiltroDelta>
|
||||
coluna_ordem?: string
|
||||
direcao_ordem?: "asc" | "desc"
|
||||
limit?: number
|
||||
offSet?: number
|
||||
}): { total: number; dados: tipoEmpreendedores[] } => {
|
||||
let whereSql = "1=1"
|
||||
const parametros: unknown[] = []
|
||||
|
||||
// 2. Filtros Avançados
|
||||
if (params.filtros) {
|
||||
const parsed = zFiltroDeltaParaQuery({ filtros: params.filtros })
|
||||
if (parsed.sql && parsed.sql !== "1=1") {
|
||||
whereSql += ` AND (${parsed.sql})`
|
||||
parametros.push(...parsed.parametros)
|
||||
}
|
||||
}
|
||||
|
||||
// Montagem da Query base
|
||||
let sql = `SELECT * FROM ? WHERE ${whereSql}`
|
||||
|
||||
// 3. Ordenação
|
||||
if (params.coluna_ordem) {
|
||||
const dir = params.direcao_ordem === "desc" ? "DESC" : "ASC"
|
||||
sql += ` ORDER BY ${params.coluna_ordem} ${dir}`
|
||||
}
|
||||
|
||||
// Obter Total antes da paginação
|
||||
const sqlCount = `SELECT COUNT(*) as t FROM ? WHERE ${whereSql}`
|
||||
const countResult = alasql(sqlCount, [_dados_empreendedores, ...parametros]) as {
|
||||
t: number
|
||||
}[]
|
||||
const total = countResult[0].t
|
||||
|
||||
// 4. Paginação
|
||||
const limit = Math.max(1, Number(params.limit ?? 10))
|
||||
const offset = Math.max(0, Number(params.offSet ?? 0))
|
||||
sql += ` LIMIT ${limit} OFFSET ${offset}`
|
||||
|
||||
console.groupCollapsed("🔍 [Alasql] Consulta executada")
|
||||
console.log("SQL:", sql)
|
||||
console.log("Parâmetros:", parametros)
|
||||
console.log("Total Encontrado:", total)
|
||||
console.groupEnd()
|
||||
|
||||
const dados = alasql(sql, [_dados_empreendedores, ...parametros]) as tipoEmpreendedores[]
|
||||
|
||||
return { total, dados }
|
||||
}
|
||||
250
src/playground/api_simulador/dados.ts
Normal file
250
src/playground/api_simulador/dados.ts
Normal file
|
|
@ -0,0 +1,250 @@
|
|||
import z from "zod"
|
||||
|
||||
let _id = 0
|
||||
/** Gera um ID incremental único para uso no seed de dados do playground. Não usar em produção. */
|
||||
export const gerarId = () => `item-${_id++}`
|
||||
|
||||
/** Schema Zod do registro de empreendedor — usado para validação e inferência de tipo no playground. */
|
||||
export const zEmpreendedores = z.object({
|
||||
codigo: z.string(),
|
||||
empreendedor: z.string(),
|
||||
empreendimento: z.string(),
|
||||
documento: z.string(),
|
||||
email: z.string(),
|
||||
telefone: z.string(),
|
||||
faturamento: z.number(),
|
||||
peso_kg: z.number(),
|
||||
areas_atuacao: z.array(
|
||||
z.enum(["mineracao", "agro", "turismo", "tecnologia", "educacao", "saude"]),
|
||||
),
|
||||
criado_em: z.string(),
|
||||
atualizado_em: z.string(),
|
||||
})
|
||||
|
||||
export type tipoEmpreendedores = z.infer<typeof zEmpreendedores>
|
||||
|
||||
// == SIMULAÇÃO DE API (CRUD) ==
|
||||
// Dados isolados (simulando um banco de dados no servidor)
|
||||
export const _dados_empreendedores: tipoEmpreendedores[] = [
|
||||
{
|
||||
codigo: gerarId(),
|
||||
empreendedor: "Maria Silva",
|
||||
empreendimento: "Doces da Maria",
|
||||
documento: "12.345.678/0001-90",
|
||||
email: "contato.comercial.super.longo@doces-da-maria-exemplo-muito-grande.com.br",
|
||||
telefone: "(11) 91234-5678",
|
||||
faturamento: 12500.5,
|
||||
peso_kg: 12.4,
|
||||
areas_atuacao: ["agro", "turismo"],
|
||||
criado_em: "2026-01-09T16:15:00Z",
|
||||
atualizado_em: "2026-01-29T15:30:00-03:00",
|
||||
},
|
||||
{
|
||||
codigo: gerarId(),
|
||||
empreendedor: "João Pereira",
|
||||
empreendimento: "Café Peregrino",
|
||||
documento: "98.765.432/0001-12",
|
||||
email: "contato@cafeperegrino.com",
|
||||
telefone: "(11) 93456-7810",
|
||||
faturamento: 8000,
|
||||
peso_kg: 6.2,
|
||||
areas_atuacao: ["tecnologia", "agro"],
|
||||
criado_em: "2026-01-02T10:00:00-03:00",
|
||||
atualizado_em: "2026-01-20T08:10:00-03:00",
|
||||
},
|
||||
{
|
||||
codigo: gerarId(),
|
||||
empreendedor: "Ana Costa",
|
||||
empreendimento: "Flor de Sal Gastronomia",
|
||||
documento: "45.678.912/0001-55",
|
||||
email: "ana@flordesal.com",
|
||||
telefone: "(21) 99876-5432",
|
||||
faturamento: 3200.75,
|
||||
peso_kg: 4.8,
|
||||
areas_atuacao: ["educacao"],
|
||||
criado_em: "2025-12-10T09:30:00-03:00",
|
||||
atualizado_em: "2026-01-15T19:20:00-03:00",
|
||||
},
|
||||
{
|
||||
codigo: gerarId(),
|
||||
empreendedor: "Rafael Lima",
|
||||
empreendimento: "Tech Agro Solutions",
|
||||
documento: "07.654.321/0001-00",
|
||||
email: "rafael@techagro.com",
|
||||
telefone: "(31) 94567-8899",
|
||||
faturamento: 15999.9,
|
||||
peso_kg: 9.1,
|
||||
areas_atuacao: ["tecnologia"],
|
||||
criado_em: "2026-01-12T12:00:00-03:00",
|
||||
atualizado_em: "2026-01-28T11:00:00-03:00",
|
||||
},
|
||||
{
|
||||
codigo: gerarId(),
|
||||
empreendedor: "Juliana Torres",
|
||||
empreendimento: "Torres Moda Sustentável",
|
||||
documento: "33.210.987/0001-44",
|
||||
email: "juliana@torresmoda.com",
|
||||
telefone: "(71) 97766-5544",
|
||||
faturamento: 4500,
|
||||
peso_kg: 3.3,
|
||||
areas_atuacao: ["turismo", "agro"],
|
||||
criado_em: "2026-01-01T00:00:00Z",
|
||||
atualizado_em: "2026-01-10T13:45:00-03:00",
|
||||
},
|
||||
{
|
||||
codigo: gerarId(),
|
||||
empreendedor: "Marcos Vieira",
|
||||
empreendimento: "Padaria Pão Quentinho",
|
||||
documento: "21.109.876/0001-32",
|
||||
email: "marcos@paoquentinho.com",
|
||||
telefone: "(48) 99654-3210",
|
||||
faturamento: 2200.4,
|
||||
peso_kg: 15.7,
|
||||
areas_atuacao: ["saude"],
|
||||
criado_em: "2026-01-05T07:25:00-03:00",
|
||||
atualizado_em: "2026-01-23T20:15:00-03:00",
|
||||
},
|
||||
{
|
||||
codigo: gerarId(),
|
||||
empreendedor: "Bianca Rocha",
|
||||
empreendimento: "Studio Beleza em Casa",
|
||||
documento: "54.321.098/0001-21",
|
||||
email: "contato@belezaemcasa.com",
|
||||
telefone: "(85) 98877-1122",
|
||||
faturamento: 990,
|
||||
peso_kg: 1.9,
|
||||
areas_atuacao: ["agro", "tecnologia"],
|
||||
criado_em: "2026-01-03T14:00:00-03:00",
|
||||
atualizado_em: "2026-01-29T09:00:00-03:00",
|
||||
},
|
||||
{
|
||||
codigo: gerarId(),
|
||||
empreendedor: "Caio Albuquerque",
|
||||
empreendimento: "Albuquerque Engenharia Verde",
|
||||
documento: "65.432.109/0001-09",
|
||||
email: "caio@engenhariaverde.com",
|
||||
telefone: "(61) 98123-4567",
|
||||
faturamento: 100000,
|
||||
peso_kg: 21.3,
|
||||
areas_atuacao: ["mineracao"],
|
||||
criado_em: "2025-11-18T16:10:00-03:00",
|
||||
atualizado_em: "2026-01-25T10:40:00-03:00",
|
||||
},
|
||||
{
|
||||
codigo: gerarId(),
|
||||
empreendedor: "Fernanda Almeida",
|
||||
empreendimento: "Faz Bem Produtos Naturais",
|
||||
documento: "87.654.210/0001-98",
|
||||
email: "fernanda@fazbem.com",
|
||||
telefone: "(41) 99777-6655",
|
||||
faturamento: 5600.2,
|
||||
peso_kg: 8.6,
|
||||
areas_atuacao: ["mineracao", "turismo"],
|
||||
criado_em: "2026-01-08T12:12:00-03:00",
|
||||
atualizado_em: "2026-01-21T17:55:00-03:00",
|
||||
},
|
||||
{
|
||||
codigo: gerarId(),
|
||||
empreendedor: "Gabriel Martins",
|
||||
empreendimento: "Martins Tech Repair",
|
||||
documento: "19.876.543/0001-76",
|
||||
email: "suporte@martinstech.com",
|
||||
telefone: "(19) 98888-9090",
|
||||
faturamento: 7550,
|
||||
peso_kg: 7.7,
|
||||
areas_atuacao: ["mineracao", "turismo"],
|
||||
criado_em: "2026-01-11T11:00:00-03:00",
|
||||
atualizado_em: "2026-01-18T11:30:00-03:00",
|
||||
},
|
||||
{
|
||||
codigo: gerarId(),
|
||||
empreendedor: "Helena Duarte",
|
||||
empreendimento: "Ateliê Costura Criativa",
|
||||
documento: "23.456.789/0001-65",
|
||||
email: "helena@costuracriativa.com",
|
||||
telefone: "(51) 98765-4433",
|
||||
faturamento: 2700,
|
||||
peso_kg: 2.5,
|
||||
areas_atuacao: ["turismo", "mineracao"],
|
||||
criado_em: "2026-01-07T08:00:00-03:00",
|
||||
atualizado_em: "2026-01-16T14:10:00-03:00",
|
||||
},
|
||||
{
|
||||
codigo: gerarId(),
|
||||
empreendedor: "Igor Santos",
|
||||
empreendimento: "Santos Consultoria Financeira",
|
||||
documento: "43.219.876/0001-54",
|
||||
email: "igor@santosconsultoria.com",
|
||||
telefone: "(31) 99332-1100",
|
||||
faturamento: 12000,
|
||||
peso_kg: 5.4,
|
||||
areas_atuacao: ["saude"],
|
||||
criado_em: "2026-01-06T10:30:00-03:00",
|
||||
atualizado_em: "2026-01-22T18:30:00-03:00",
|
||||
},
|
||||
{
|
||||
codigo: gerarId(),
|
||||
empreendedor: "Jéssica Nunes",
|
||||
empreendimento: "Nunes Eventos & Decoração",
|
||||
documento: "09.876.543/0001-33",
|
||||
email: "jessica@nunesdecora.com",
|
||||
telefone: "(62) 99922-3344",
|
||||
faturamento: 3300.1,
|
||||
peso_kg: 4.1,
|
||||
areas_atuacao: ["turismo"],
|
||||
criado_em: "2026-01-04T13:30:00-03:00",
|
||||
atualizado_em: "2026-01-26T09:20:00-03:00",
|
||||
},
|
||||
{
|
||||
codigo: gerarId(),
|
||||
empreendedor: "Leonardo Prado",
|
||||
empreendimento: "Prado Bike Courier",
|
||||
documento: "72.345.098/0001-27",
|
||||
email: "contato@pradobike.com",
|
||||
telefone: "(47) 98444-6677",
|
||||
faturamento: 4100,
|
||||
peso_kg: 6.8,
|
||||
areas_atuacao: ["mineracao"],
|
||||
criado_em: "2026-01-02T18:40:00-03:00",
|
||||
atualizado_em: "2026-01-27T12:00:00-03:00",
|
||||
},
|
||||
{
|
||||
codigo: gerarId(),
|
||||
empreendedor: "Marina Lopes",
|
||||
empreendimento: "Lopes Design Digital",
|
||||
documento: "56.789.012/0001-11",
|
||||
email: "marina@lopesdesign.com",
|
||||
telefone: "(27) 99911-2233",
|
||||
faturamento: 2100,
|
||||
peso_kg: 3.7,
|
||||
areas_atuacao: ["agro"],
|
||||
criado_em: "2026-01-09T09:00:00-03:00",
|
||||
atualizado_em: "2026-01-19T15:35:00-03:00",
|
||||
},
|
||||
{
|
||||
codigo: gerarId(),
|
||||
empreendedor: "Nicolas Teixeira",
|
||||
empreendimento: "QualiVida Fitness",
|
||||
documento: "34.567.890/0001-02",
|
||||
email: "nicolas@qualivida.com",
|
||||
telefone: "(92) 99456-7788",
|
||||
faturamento: 8600.9,
|
||||
peso_kg: 11.2,
|
||||
areas_atuacao: ["agro", "saude"],
|
||||
criado_em: "2026-01-12T19:15:00-03:00",
|
||||
atualizado_em: "2026-01-29T10:50:00-03:00",
|
||||
},
|
||||
{
|
||||
codigo: gerarId(),
|
||||
empreendedor: "Olívia Azevedo",
|
||||
empreendimento: "Azevedo Pet Care",
|
||||
documento: "88.901.234/0001-45",
|
||||
email: "olivia@petcare.com",
|
||||
telefone: "(16) 99788-6655",
|
||||
faturamento: 1900.3,
|
||||
peso_kg: 2.2,
|
||||
areas_atuacao: ["agro"],
|
||||
criado_em: "2026-01-10T15:00:00-03:00",
|
||||
atualizado_em: "2026-01-14T16:00:00-03:00",
|
||||
},
|
||||
]
|
||||
114
src/playground/api_simulador/index.ts
Normal file
114
src/playground/api_simulador/index.ts
Normal file
|
|
@ -0,0 +1,114 @@
|
|||
import { respostaComuns, type tipoResposta } from "p-respostas"
|
||||
import type z from "zod"
|
||||
import { executarConsultaAlasql } from "./banco"
|
||||
import { _dados_empreendedores, type tipoEmpreendedores } from "./dados"
|
||||
import { type tipoFuncaoApiSimulacao, zApiSimulacao } from "./tipagem"
|
||||
|
||||
const delay = (ms: number) => new Promise((r) => setTimeout(r, ms * Math.random() + 2000))
|
||||
|
||||
// -- API PÚBLICA SIMULADA --
|
||||
|
||||
const api_ler: tipoFuncaoApiSimulacao<"listar"> = async (params) => {
|
||||
// simula latência de rede
|
||||
|
||||
const { total, dados } = executarConsultaAlasql({
|
||||
filtros: params.filtros,
|
||||
coluna_ordem: params.coluna_ordem as string | undefined,
|
||||
direcao_ordem: params.direcao_ordem,
|
||||
limit: params.limit,
|
||||
offSet: params.offSet,
|
||||
})
|
||||
|
||||
return respostaComuns.valor({
|
||||
total,
|
||||
dados,
|
||||
})
|
||||
}
|
||||
|
||||
const api_criar: tipoFuncaoApiSimulacao<"criar"> = async (dados) => {
|
||||
if (!dados.empreendedor) {
|
||||
return respostaComuns.erro("Empreendedor é obrigatório")
|
||||
}
|
||||
|
||||
const novo: tipoEmpreendedores = {
|
||||
codigo: Math.random().toString(),
|
||||
empreendedor: dados.empreendedor || "Novo",
|
||||
empreendimento: dados.empreendimento || "Novo Emp",
|
||||
documento: dados.documento || "00.000.000/0000-00",
|
||||
email: dados.email || "novo@email.com",
|
||||
telefone: dados.telefone || "(00) 0000-0000",
|
||||
faturamento: dados.faturamento || 0,
|
||||
peso_kg: dados.peso_kg || 0,
|
||||
areas_atuacao: dados.areas_atuacao || [],
|
||||
criado_em: new Date().toISOString(),
|
||||
atualizado_em: new Date().toISOString(),
|
||||
}
|
||||
_dados_empreendedores.unshift(novo)
|
||||
return respostaComuns.valor(novo)
|
||||
}
|
||||
|
||||
const api_editar: tipoFuncaoApiSimulacao<"editar"> = async (dados) => {
|
||||
const idx = _dados_empreendedores.find((l) => l.codigo === dados.codigo)
|
||||
if (!idx) return respostaComuns.erro("Registro não encontrado")
|
||||
Object.assign(idx, dados.novosDados)
|
||||
return respostaComuns.valor(idx)
|
||||
}
|
||||
|
||||
const api_deletar: tipoFuncaoApiSimulacao<"deletar"> = async (dados) => {
|
||||
const idx = _dados_empreendedores.findIndex((l) => l.codigo === dados.codigo)
|
||||
if (idx !== -1) {
|
||||
_dados_empreendedores.splice(idx, 1)
|
||||
return respostaComuns.valor("Registro deletado com sucesso")
|
||||
}
|
||||
return respostaComuns.erro("Registro não encontrado")
|
||||
}
|
||||
|
||||
const ApiSimulacao = async <tTipo extends keyof z.infer<typeof zApiSimulacao>>(
|
||||
tIPO: tTipo,
|
||||
req: z.infer<typeof zApiSimulacao>[tTipo]["req"],
|
||||
Callback?: (
|
||||
req: z.infer<typeof zApiSimulacao>[tTipo]["req"],
|
||||
res: tipoResposta<z.infer<typeof zApiSimulacao>[tTipo]["res"]>,
|
||||
) => void,
|
||||
): Promise<tipoResposta<z.infer<typeof zApiSimulacao>[tTipo]["res"]>> => {
|
||||
await delay((2000 * Math.random() + 200) | 0)
|
||||
|
||||
let retorno: tipoResposta<z.infer<typeof zApiSimulacao>[tTipo]["res"]> =
|
||||
respostaComuns.erro("Não entrou!")
|
||||
const zSchema = zApiSimulacao.shape[tIPO]
|
||||
|
||||
try {
|
||||
await zSchema.shape.req.parseAsync(req)
|
||||
|
||||
switch (tIPO) {
|
||||
case "listar":
|
||||
retorno = await api_ler(req as z.infer<typeof zApiSimulacao>["listar"]["req"])
|
||||
break
|
||||
case "criar":
|
||||
retorno = await api_criar(req as z.infer<typeof zApiSimulacao>["criar"]["req"])
|
||||
break
|
||||
case "editar":
|
||||
retorno = await api_editar(req as z.infer<typeof zApiSimulacao>["editar"]["req"])
|
||||
break
|
||||
case "deletar":
|
||||
retorno = await api_deletar(req as z.infer<typeof zApiSimulacao>["deletar"]["req"])
|
||||
break
|
||||
default:
|
||||
retorno = respostaComuns.erro("Tipo de API não encontrado!")
|
||||
}
|
||||
|
||||
if (retorno.eCerto) {
|
||||
await zSchema.shape.res.parseAsync(retorno.valor)
|
||||
}
|
||||
} catch (error) {
|
||||
retorno = respostaComuns.erro((error as Error).message)
|
||||
}
|
||||
|
||||
if (Callback) {
|
||||
Callback(req, retorno)
|
||||
}
|
||||
|
||||
return retorno
|
||||
}
|
||||
|
||||
export { ApiSimulacao }
|
||||
52
src/playground/api_simulador/tipagem.ts
Normal file
52
src/playground/api_simulador/tipagem.ts
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
import { zFiltroDelta } from "p-comuns"
|
||||
import type { tipoResposta } from "p-respostas"
|
||||
import z from "zod"
|
||||
import { type tipoEmpreendedores, zEmpreendedores } from "./dados"
|
||||
|
||||
const baseApi = <tTipo extends string, tReq extends z.ZodTypeAny, tRes extends z.ZodTypeAny>(
|
||||
tipo: tTipo,
|
||||
req: tReq,
|
||||
res: tRes,
|
||||
) =>
|
||||
z.object({
|
||||
tipo: z.literal(tipo),
|
||||
req: req,
|
||||
res: res,
|
||||
})
|
||||
|
||||
export const zApiSimulacao = z.object({
|
||||
listar: baseApi(
|
||||
"listar",
|
||||
z.object({
|
||||
filtros: zFiltroDelta.optional(),
|
||||
coluna_ordem: z.custom<keyof tipoEmpreendedores>().optional(),
|
||||
direcao_ordem: z.enum(["asc", "desc"]).optional(),
|
||||
limit: z.number().optional(),
|
||||
offSet: z.number().optional(),
|
||||
}),
|
||||
z.object({
|
||||
total: z.number(),
|
||||
dados: z.array(zEmpreendedores),
|
||||
}),
|
||||
),
|
||||
criar: baseApi("criar", zEmpreendedores, zEmpreendedores),
|
||||
editar: baseApi(
|
||||
"editar",
|
||||
z.object({
|
||||
codigo: z.string(),
|
||||
novosDados: zEmpreendedores.partial(),
|
||||
}),
|
||||
zEmpreendedores,
|
||||
),
|
||||
deletar: baseApi(
|
||||
"deletar",
|
||||
z.object({
|
||||
codigo: z.string(),
|
||||
}),
|
||||
z.string(),
|
||||
),
|
||||
})
|
||||
|
||||
export type tipoFuncaoApiSimulacao<tTipo extends keyof z.infer<typeof zApiSimulacao>> = (
|
||||
dados: z.infer<typeof zApiSimulacao>[tTipo]["req"],
|
||||
) => Promise<tipoResposta<z.infer<typeof zApiSimulacao>[tTipo]["res"]>>
|
||||
|
|
@ -121,9 +121,7 @@ export default defineComponent({
|
|||
function itensFiltrados(itens: Card[]) {
|
||||
const q = filtro.value.trim().toLowerCase()
|
||||
if (!q) return itens
|
||||
return itens.filter((i) =>
|
||||
`${i.titulo} ${i.cliente} ${i.valor}`.toLowerCase().includes(q),
|
||||
)
|
||||
return itens.filter((i) => `${i.titulo} ${i.cliente} ${i.valor}`.toLowerCase().includes(q))
|
||||
}
|
||||
|
||||
function criar() {
|
||||
|
|
|
|||
|
|
@ -125,12 +125,7 @@
|
|||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from "vue"
|
||||
import {
|
||||
EliEntradaNumero,
|
||||
EliEntradaParagrafo,
|
||||
EliEntradaSelecao,
|
||||
EliEntradaTexto,
|
||||
} from "../index"
|
||||
import { EliEntradaNumero, EliEntradaParagrafo, EliEntradaSelecao, EliEntradaTexto } from "../index"
|
||||
|
||||
export default defineComponent({
|
||||
name: "EntradasPlayground",
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ import * as directives from "vuetify/directives"
|
|||
// Estilos globais da lib (font-face + variáveis de fonte)
|
||||
// Importamos DEPOIS do Vuetify para garantir que `--v-font-family` seja sobrescrito.
|
||||
import "../styles/eli-vue-fonts.css"
|
||||
import "../design-tokens.css"
|
||||
|
||||
const themeDefault = {
|
||||
dark: false,
|
||||
|
|
@ -173,22 +174,15 @@ if (import.meta.env.DEV) {
|
|||
// eslint-disable-next-line no-console
|
||||
console.log(
|
||||
"[eli-vue] --v-font-family:",
|
||||
getComputedStyle(document.documentElement).getPropertyValue(
|
||||
"--v-font-family",
|
||||
),
|
||||
getComputedStyle(document.documentElement).getPropertyValue("--v-font-family"),
|
||||
)
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(
|
||||
"[eli-vue] --eli-font-family:",
|
||||
getComputedStyle(document.documentElement).getPropertyValue(
|
||||
"--eli-font-family",
|
||||
),
|
||||
getComputedStyle(document.documentElement).getPropertyValue("--eli-font-family"),
|
||||
)
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(
|
||||
"[eli-vue] body font-family:",
|
||||
getComputedStyle(document.body).fontFamily,
|
||||
)
|
||||
console.log("[eli-vue] body font-family:", getComputedStyle(document.body).fontFamily)
|
||||
}
|
||||
|
||||
app.use(vuetify).mount("#app")
|
||||
|
|
|
|||
|
|
@ -2,15 +2,14 @@
|
|||
<section class="stack">
|
||||
<h2>EliTabela</h2>
|
||||
|
||||
<!-- Exemplo: botão de ação "Novo" no cabeçalho que adiciona uma linha -->
|
||||
<div style="margin-bottom: 12px">
|
||||
<strong>Selecionados:</strong> {{ selecionados.map(s => s.empreendedor).join(', ') || 'Nenhum' }}
|
||||
</div>
|
||||
<EliTabela :tabela="tabelaOk" :key="versaoTabelaOk" v-model:selecionados="selecionados" />
|
||||
<EliTabela :tabela="tabelaOk" :key="versaoTabelaOk" v-model:selecionados="selecionados" esconderDebug />
|
||||
|
||||
<EliTabela :tabela="tabelaVazia" />
|
||||
<EliTabela :tabela="tabelaVazia" esconderDebug />
|
||||
|
||||
<EliTabela :tabela="tabelaErro" />
|
||||
<EliTabela :tabela="tabelaErro" esconderDebug />
|
||||
|
||||
<!-- Modal de simulação de cadastro -->
|
||||
<v-dialog v-model="mostrarModalCadastro" max-width="500px">
|
||||
|
|
@ -65,359 +64,15 @@
|
|||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { BadgeCheck, Eye, Pencil, Plus, Trash2 } from "@lucide/vue"
|
||||
import { criarFiltro26, type tipoFiltro26 } from "p-comuns"
|
||||
import { codigosResposta } from "p-respostas"
|
||||
import { defineComponent, ref } from "vue"
|
||||
import type { ComponenteEntrada } from "@/componentes/EliEntrada/tiposEntradas"
|
||||
import type { tipoEliTabelaConsulta } from "@/componentes/EliTabela"
|
||||
import { celulaTabela, EliTabela } from "@/componentes/EliTabela"
|
||||
|
||||
import { api_criar, api_deletar, api_ler, type Linha } from "./api_simulador"
|
||||
import { defineComponent } from "vue"
|
||||
import { EliTabela } from "@/componentes/EliTabela"
|
||||
import { useTabelaPlayground } from "./useTabelaPlayground"
|
||||
|
||||
export default defineComponent({
|
||||
name: "TabelaPlayground",
|
||||
components: { EliTabela },
|
||||
setup() {
|
||||
const acoesLinha: tipoEliTabelaConsulta<Linha>["acoesLinha"] = [
|
||||
{
|
||||
icone: Eye,
|
||||
cor: "#2563eb",
|
||||
rotulo: "Detalhes",
|
||||
acao: (linha) => {
|
||||
console.log("Visualizar detalhes de", linha.empreendedor)
|
||||
},
|
||||
},
|
||||
{
|
||||
icone: Trash2,
|
||||
cor: "#dc2626",
|
||||
rotulo: "Remover",
|
||||
acao: (linha) => {
|
||||
removerCadastro(linha)
|
||||
},
|
||||
exibir: (linha) => !linha.empreendimento.toLowerCase().includes("café"),
|
||||
},
|
||||
]
|
||||
|
||||
const versaoTabelaOk = ref(0)
|
||||
const selecionados = ref<Linha[]>([])
|
||||
const mostrarModalCadastro = ref(false)
|
||||
const salvando = ref(false)
|
||||
const novoCadastro = ref({
|
||||
empreendedor: "",
|
||||
empreendimento: "",
|
||||
documento: "",
|
||||
email: "",
|
||||
telefone: "",
|
||||
faturamento: 0,
|
||||
peso_kg: 0,
|
||||
})
|
||||
const erroCadastro = ref<string | undefined>(undefined)
|
||||
|
||||
// Armazena a função de atualização da tabela atual (vinda da ação do cabeçalho)
|
||||
let atualizarTabelaCallback: (() => Promise<void>) | undefined
|
||||
|
||||
// A ação do cabeçalho recebe { atualizarConsulta, editarLista, ... }
|
||||
// biome-ignore lint/suspicious/noExplicitAny: playground
|
||||
function abrirModalCadastro(params?: any) {
|
||||
if (params?.atualizarConsulta) {
|
||||
atualizarTabelaCallback = params.atualizarConsulta
|
||||
}
|
||||
|
||||
novoCadastro.value = {
|
||||
empreendedor: "",
|
||||
empreendimento: "",
|
||||
documento: "",
|
||||
email: "",
|
||||
telefone: "",
|
||||
faturamento: 0,
|
||||
peso_kg: 0,
|
||||
}
|
||||
erroCadastro.value = undefined
|
||||
mostrarModalCadastro.value = true
|
||||
}
|
||||
|
||||
async function salvarNovoCadastro() {
|
||||
salvando.value = true
|
||||
erroCadastro.value = undefined
|
||||
try {
|
||||
const resposta = await api_criar(novoCadastro.value)
|
||||
if (resposta.eErro) {
|
||||
erroCadastro.value = resposta.mensagem
|
||||
return
|
||||
}
|
||||
|
||||
// PADRÃO RECOMENDADO:
|
||||
// Se temos o callback de atualização, usamos ele para recarregar os dados na tabela
|
||||
// sem precisar remontar o componente (sem alterar key).
|
||||
if (atualizarTabelaCallback) {
|
||||
await atualizarTabelaCallback()
|
||||
} else {
|
||||
// Fallback (caso não tenha vindo pela ação da tabela): força re-render
|
||||
versaoTabelaOk.value++
|
||||
}
|
||||
|
||||
mostrarModalCadastro.value = false
|
||||
} catch (e) {
|
||||
erroCadastro.value = String(e)
|
||||
} finally {
|
||||
salvando.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function removerCadastro(linha: Linha) {
|
||||
// Exemplo de uso do api_deletar
|
||||
// Não temos um ID único confiável além das propriedades, vamos usar empreendedor como chave neste exemplo simples
|
||||
if (confirm(`Tem certeza que deseja remover ${linha.empreendedor}?`)) {
|
||||
api_deletar("empreendedor", linha.empreendedor).then(() => {
|
||||
versaoTabelaOk.value++
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const tabelaOk: tipoEliTabelaConsulta<Linha> = {
|
||||
nome: "ExemploPlaygroundData",
|
||||
registros_por_consulta: 10,
|
||||
mostrarCaixaDeBusca: true,
|
||||
mostrar_selecao: true,
|
||||
chave_selecao: "empreendedor",
|
||||
acoesTabela: [
|
||||
{
|
||||
posicao: "superior",
|
||||
icone: Plus,
|
||||
cor: "#16a34a",
|
||||
rotulo: "Novo (Superior)",
|
||||
acao: abrirModalCadastro,
|
||||
},
|
||||
{
|
||||
posicao: "inferior",
|
||||
icone: Plus,
|
||||
cor: "#2563eb",
|
||||
rotulo: "Novo (Inferior)",
|
||||
acao: abrirModalCadastro,
|
||||
},
|
||||
],
|
||||
|
||||
colunas: [
|
||||
{
|
||||
rotulo: "Empreendedor",
|
||||
celula: (l) =>
|
||||
celulaTabela("textoSimples", { texto: l.empreendedor }),
|
||||
coluna_ordem: "empreendedor",
|
||||
visivel: true,
|
||||
},
|
||||
{
|
||||
rotulo: "Empreendimento",
|
||||
celula: (l) =>
|
||||
celulaTabela("textoSimples", { texto: l.empreendimento }),
|
||||
coluna_ordem: "empreendimento",
|
||||
visivel: true,
|
||||
},
|
||||
{
|
||||
rotulo: "Documento",
|
||||
celula: (l) => celulaTabela("textoSimples", { texto: l.documento }),
|
||||
coluna_ordem: "documento",
|
||||
visivel: false,
|
||||
},
|
||||
{
|
||||
rotulo: "E-mail",
|
||||
celula: (l) =>
|
||||
celulaTabela("textoTruncado", {
|
||||
texto: l.email,
|
||||
acao: () => {
|
||||
// Exemplo de ação: poderia abrir detalhes
|
||||
alert(`Clicou em ${l.email}`)
|
||||
},
|
||||
}),
|
||||
coluna_ordem: "email",
|
||||
visivel: true,
|
||||
},
|
||||
{
|
||||
rotulo: "Telefone",
|
||||
celula: (l) => celulaTabela("textoSimples", { texto: l.telefone }),
|
||||
coluna_ordem: "telefone",
|
||||
visivel: true,
|
||||
},
|
||||
{
|
||||
rotulo: "Faturamento",
|
||||
celula: (l) =>
|
||||
celulaTabela("numero", {
|
||||
numero: l.faturamento,
|
||||
prefixo: "R$",
|
||||
// Exemplo de ação (clicável)
|
||||
acao: () =>
|
||||
alert(`Faturamento de ${l.empreendedor}: R$ ${l.faturamento}`),
|
||||
}),
|
||||
visivel: true,
|
||||
},
|
||||
{
|
||||
rotulo: "Peso",
|
||||
celula: (l) =>
|
||||
celulaTabela("numero", {
|
||||
numero: l.peso_kg,
|
||||
sufixo: "kg",
|
||||
}),
|
||||
visivel: false,
|
||||
},
|
||||
{
|
||||
rotulo: "Status",
|
||||
celula: (l) =>
|
||||
celulaTabela("tags", {
|
||||
opcoes: [
|
||||
{
|
||||
rotulo: "Ativo",
|
||||
cor: "success",
|
||||
icone: BadgeCheck,
|
||||
},
|
||||
{
|
||||
rotulo: "Editar",
|
||||
cor: "primary",
|
||||
icone: Pencil,
|
||||
acao: () => {
|
||||
alert(`Editar ${l.empreendedor}`)
|
||||
},
|
||||
},
|
||||
],
|
||||
}),
|
||||
visivel: true,
|
||||
},
|
||||
{
|
||||
rotulo: "Criado em",
|
||||
celula: (l) =>
|
||||
celulaTabela("data", {
|
||||
valor: l.criado_em,
|
||||
formato: "data",
|
||||
}),
|
||||
visivel: false,
|
||||
},
|
||||
{
|
||||
rotulo: "Atualizado",
|
||||
celula: (l) =>
|
||||
celulaTabela("data", {
|
||||
valor: l.atualizado_em,
|
||||
formato: "relativo",
|
||||
acao: () => alert(`Atualizado em: ${l.atualizado_em}`),
|
||||
}),
|
||||
visivel: true,
|
||||
},
|
||||
],
|
||||
acoesLinha: acoesLinha,
|
||||
filtroAvancado: [
|
||||
{
|
||||
chave: "empreendedor",
|
||||
rotulo: "Empreendedor",
|
||||
// biome-ignore lint/suspicious/noExplicitAny: <explanation>
|
||||
filtro: (valor: unknown) =>
|
||||
criarFiltro26({
|
||||
// biome-ignore lint/suspicious/noExplicitAny: playground
|
||||
empreendedor: { like: valor as string } as any,
|
||||
}) as tipoFiltro26<Linha>,
|
||||
entrada: ["texto", { rotulo: "Empreendedor" }] as ComponenteEntrada,
|
||||
},
|
||||
{
|
||||
chave: "documento",
|
||||
rotulo: "Documento",
|
||||
// biome-ignore lint/suspicious/noExplicitAny: <explanation>
|
||||
filtro: (valor: string) =>
|
||||
criarFiltro26({
|
||||
// biome-ignore lint/suspicious/noExplicitAny: playground
|
||||
documento: { like: valor as string } as any,
|
||||
}) as tipoFiltro26<Linha>,
|
||||
entrada: [
|
||||
"texto",
|
||||
{ rotulo: "Documento", formato: "cpfCnpj" },
|
||||
] as ComponenteEntrada,
|
||||
},
|
||||
{
|
||||
chave: "email",
|
||||
rotulo: "E-mail",
|
||||
// biome-ignore lint/suspicious/noExplicitAny: <explanation>
|
||||
filtro: (valor: string) =>
|
||||
criarFiltro26({
|
||||
// biome-ignore lint/suspicious/noExplicitAny: playground
|
||||
email: { like: valor as string } as any,
|
||||
}) as tipoFiltro26<Linha>,
|
||||
entrada: [
|
||||
"texto",
|
||||
{ rotulo: "E-mail", formato: "email" },
|
||||
] as ComponenteEntrada,
|
||||
},
|
||||
],
|
||||
consulta: async (parametrosConsulta) => {
|
||||
try {
|
||||
const resposta = await api_ler(parametrosConsulta)
|
||||
return {
|
||||
cod: codigosResposta.sucesso,
|
||||
eCerto: true,
|
||||
eErro: false,
|
||||
mensagem: undefined,
|
||||
valor: {
|
||||
quantidade: resposta.total,
|
||||
valores: resposta.dados,
|
||||
},
|
||||
}
|
||||
} catch (error) {
|
||||
return {
|
||||
cod: codigosResposta.erroConhecido,
|
||||
eCerto: false,
|
||||
eErro: true,
|
||||
mensagem: String(error),
|
||||
valor: undefined,
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
const tabelaVazia: tipoEliTabelaConsulta<Linha> = {
|
||||
nome: "Exemplo Vazio",
|
||||
registros_por_consulta: 10,
|
||||
mostrarCaixaDeBusca: true,
|
||||
colunas: tabelaOk.colunas,
|
||||
consulta: async (_parametrosConsulta) => {
|
||||
return {
|
||||
cod: codigosResposta.sucesso,
|
||||
eCerto: true,
|
||||
eErro: false,
|
||||
mensagem: undefined,
|
||||
valor: {
|
||||
quantidade: 0,
|
||||
valores: [],
|
||||
},
|
||||
}
|
||||
},
|
||||
mensagemVazio: "Nada para mostrar aqui.",
|
||||
acoesLinha: acoesLinha,
|
||||
}
|
||||
|
||||
const tabelaErro: tipoEliTabelaConsulta<Linha> = {
|
||||
nome: "Exemplo",
|
||||
registros_por_consulta: tabelaOk.registros_por_consulta,
|
||||
mostrarCaixaDeBusca: tabelaOk.mostrarCaixaDeBusca,
|
||||
colunas: tabelaOk.colunas,
|
||||
acoesLinha: acoesLinha,
|
||||
consulta: async (_parametrosConsulta) => {
|
||||
return {
|
||||
cod: codigosResposta.erroConhecido,
|
||||
eCerto: false,
|
||||
eErro: true,
|
||||
mensagem: "Falha ao buscar dados",
|
||||
valor: undefined,
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
return {
|
||||
tabelaOk,
|
||||
tabelaVazia,
|
||||
tabelaErro,
|
||||
versaoTabelaOk,
|
||||
mostrarModalCadastro,
|
||||
salvando,
|
||||
novoCadastro,
|
||||
salvarNovoCadastro,
|
||||
erroCadastro,
|
||||
selecionados,
|
||||
}
|
||||
return useTabelaPlayground()
|
||||
},
|
||||
})
|
||||
</script>
|
||||
|
|
@ -429,6 +84,15 @@ export default defineComponent({
|
|||
}
|
||||
|
||||
.stack :deep(.eli-tabela) {
|
||||
max-width: 900px;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.debug {
|
||||
padding: 12px;
|
||||
background: rgba(0, 0, 0, 0.04);
|
||||
border-radius: 8px;
|
||||
overflow: auto;
|
||||
font-size: 12px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
171
src/playground/useTabelaPlayground.ts
Normal file
171
src/playground/useTabelaPlayground.ts
Normal file
|
|
@ -0,0 +1,171 @@
|
|||
/**
|
||||
* useTabelaPlayground
|
||||
* Composable principal do playground da EliTabela.
|
||||
* Gerencia estado de CRUD (modal, formulário, erros) e compõe as 3 instâncias de tabela de demonstração.
|
||||
* A configuração de colunas, filtros e consulta está em `useTabelaPlaygroundConfig.ts`.
|
||||
*
|
||||
* @returns Estado reativo, configurações de tabela e ações de CRUD para o template
|
||||
* @see useTabelaPlaygroundConfig.ts — definições de colunas, filtros e consulta
|
||||
* @see tabela.playground.vue — componente que consome este composable
|
||||
*/
|
||||
import { Plus } from "@lucide/vue"
|
||||
import { codigosResposta } from "p-respostas"
|
||||
import { inject, ref } from "vue"
|
||||
import type { tipoEliTabelaConsulta } from "@/componentes/EliTabela"
|
||||
import { PLAYGROUND_CONSOLE_KEY, type PlaygroundConsole } from "./App.vue"
|
||||
import { ApiSimulacao } from "./api_simulador"
|
||||
import type { tipoEmpreendedores } from "./api_simulador/dados"
|
||||
import { colunasTabela, criarAcoesLinha, filtrosAvancados } from "./useTabelaPlaygroundConfig"
|
||||
import { consultar } from "./useTabelaPlaygroundConsultar"
|
||||
|
||||
export function useTabelaPlayground() {
|
||||
const playgroundConsole = inject<PlaygroundConsole>(PLAYGROUND_CONSOLE_KEY)
|
||||
|
||||
function adicionarLog(acao: string, req: unknown, res: unknown) {
|
||||
if (playgroundConsole) playgroundConsole.adicionarLog(`ApiSimulacao: ${acao}`, req, res)
|
||||
}
|
||||
|
||||
const versaoTabelaOk = ref(0)
|
||||
const selecionados = ref<tipoEmpreendedores[]>([])
|
||||
const mostrarModalCadastro = ref(false)
|
||||
const salvando = ref(false)
|
||||
const novoCadastro = ref<Omit<tipoEmpreendedores, "codigo" | "criado_em" | "atualizado_em">>({
|
||||
empreendedor: "",
|
||||
empreendimento: "",
|
||||
documento: "",
|
||||
email: "",
|
||||
telefone: "",
|
||||
faturamento: 0,
|
||||
peso_kg: 0,
|
||||
areas_atuacao: [],
|
||||
})
|
||||
const erroCadastro = ref<string | undefined>(undefined)
|
||||
|
||||
let atualizarTabelaCallback: (() => Promise<void>) | undefined
|
||||
|
||||
// biome-ignore lint/suspicious/noExplicitAny: playground — parâmetros dinâmicos da ação de tabela
|
||||
function abrirModalCadastro(params?: any) {
|
||||
if (params?.atualizarConsulta) atualizarTabelaCallback = params.atualizarConsulta
|
||||
novoCadastro.value = {
|
||||
empreendedor: "",
|
||||
empreendimento: "",
|
||||
documento: "",
|
||||
email: "",
|
||||
telefone: "",
|
||||
faturamento: 0,
|
||||
peso_kg: 0,
|
||||
areas_atuacao: [],
|
||||
}
|
||||
erroCadastro.value = undefined
|
||||
mostrarModalCadastro.value = true
|
||||
}
|
||||
|
||||
async function salvarNovoCadastro() {
|
||||
salvando.value = true
|
||||
erroCadastro.value = undefined
|
||||
try {
|
||||
const payload: tipoEmpreendedores = {
|
||||
...novoCadastro.value,
|
||||
codigo: "novo",
|
||||
criado_em: new Date().toISOString(),
|
||||
atualizado_em: new Date().toISOString(),
|
||||
}
|
||||
const resposta = await ApiSimulacao("criar", payload, (req, res) => {
|
||||
console.groupCollapsed("🌐 [ApiSimulacao] criar")
|
||||
console.info("Req:", req)
|
||||
console.info("Res:", res)
|
||||
console.groupEnd()
|
||||
adicionarLog("criar", req, res)
|
||||
})
|
||||
if (resposta.eErro) {
|
||||
erroCadastro.value = resposta.mensagem
|
||||
return
|
||||
}
|
||||
if (atualizarTabelaCallback) await atualizarTabelaCallback()
|
||||
else versaoTabelaOk.value++
|
||||
mostrarModalCadastro.value = false
|
||||
} catch (e) {
|
||||
erroCadastro.value = String(e)
|
||||
} finally {
|
||||
salvando.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function removerCadastro(linha: tipoEmpreendedores) {
|
||||
if (confirm(`Tem certeza que deseja remover ${linha.empreendedor}?`)) {
|
||||
ApiSimulacao("deletar", { codigo: linha.codigo }, (req, res) => {
|
||||
console.groupCollapsed("🌐 [ApiSimulacao] deletar")
|
||||
console.info("Req:", req)
|
||||
console.info("Res:", res)
|
||||
console.groupEnd()
|
||||
adicionarLog("deletar", req, res)
|
||||
}).then(() => {
|
||||
versaoTabelaOk.value++
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const acoesLinha = criarAcoesLinha(removerCadastro)
|
||||
const consultarFn: tipoEliTabelaConsulta<tipoEmpreendedores>["consulta"] = (params) =>
|
||||
consultar(params, adicionarLog)
|
||||
|
||||
const tabelaOk: tipoEliTabelaConsulta<tipoEmpreendedores> = {
|
||||
nome: "ExemploPlaygroundData",
|
||||
registros_por_consulta: 10,
|
||||
mostrarCaixaDeBusca: true,
|
||||
mostrar_selecao: true,
|
||||
chave_selecao: "empreendedor",
|
||||
acoesTabela: [
|
||||
{ posicao: "superior", icone: Plus, rotulo: "Novo (Superior)", acao: abrirModalCadastro },
|
||||
{ posicao: "inferior", icone: Plus, rotulo: "Novo (Inferior)", acao: abrirModalCadastro },
|
||||
],
|
||||
colunas: colunasTabela,
|
||||
acoesLinha,
|
||||
filtroAvancado: filtrosAvancados,
|
||||
consulta: consultarFn,
|
||||
}
|
||||
|
||||
const tabelaVazia: tipoEliTabelaConsulta<tipoEmpreendedores> = {
|
||||
nome: "Exemplo Vazio",
|
||||
registros_por_consulta: 10,
|
||||
mostrarCaixaDeBusca: true,
|
||||
colunas: colunasTabela,
|
||||
mensagemVazio: "Nada para mostrar aqui.",
|
||||
acoesLinha,
|
||||
consulta: async () => ({
|
||||
cod: codigosResposta.sucesso,
|
||||
eCerto: true,
|
||||
eErro: false,
|
||||
mensagem: undefined,
|
||||
valor: { quantidade: 0, valores: [] },
|
||||
}),
|
||||
}
|
||||
|
||||
const tabelaErro: tipoEliTabelaConsulta<tipoEmpreendedores> = {
|
||||
nome: "Exemplo",
|
||||
registros_por_consulta: tabelaOk.registros_por_consulta,
|
||||
mostrarCaixaDeBusca: tabelaOk.mostrarCaixaDeBusca,
|
||||
colunas: colunasTabela,
|
||||
acoesLinha,
|
||||
consulta: async () => ({
|
||||
cod: codigosResposta.erroConhecido,
|
||||
eCerto: false,
|
||||
eErro: true,
|
||||
mensagem: "Falha ao buscar dados",
|
||||
valor: undefined,
|
||||
}),
|
||||
}
|
||||
|
||||
return {
|
||||
tabelaOk,
|
||||
tabelaVazia,
|
||||
tabelaErro,
|
||||
versaoTabelaOk,
|
||||
mostrarModalCadastro,
|
||||
salvando,
|
||||
novoCadastro,
|
||||
salvarNovoCadastro,
|
||||
erroCadastro,
|
||||
selecionados,
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue