From fc45073c41f425b3e46e47f362eb8c5a92005c7b Mon Sep 17 00:00:00 2001 From: Gustavo Soares Date: Thu, 12 Mar 2026 16:47:55 -0300 Subject: [PATCH 01/12] feat: reorganizando conhecimento de criacao de formulario --- .gitignore | 2 +- .../project-knowledge/form-creation.md | 169 ++++++++++++++++++ frontend/CLAUDE.md | 6 + 3 files changed, 176 insertions(+), 1 deletion(-) create mode 100644 frontend/.claude/project-knowledge/form-creation.md create mode 100644 frontend/CLAUDE.md diff --git a/.gitignore b/.gitignore index 430873a3b..535009807 100644 --- a/.gitignore +++ b/.gitignore @@ -9,4 +9,4 @@ openapitools.json .DS_Store .vscode/settings.json -.claude/ \ No newline at end of file +./.claude/ \ No newline at end of file diff --git a/frontend/.claude/project-knowledge/form-creation.md b/frontend/.claude/project-knowledge/form-creation.md new file mode 100644 index 000000000..83ae61da7 --- /dev/null +++ b/frontend/.claude/project-knowledge/form-creation.md @@ -0,0 +1,169 @@ +## Formulários + +### Schema Yup + +- Schemas ficam em `src/consts/formSchemas/.ts` +- Importar primitivos de `./initSchema` (não direto do `yup`): + ```ts + import { + object, + string, + number, + array, + boolean, + date, + mixed, + } from "./initSchema"; + ``` +- Sempre usar `.label('Nome do Campo')` em cada campo — o `SmaeLabel` extrai isso automaticamente +- Campos opcionais: `.nullable()` ou `.nullableOuVazio()` +- Campos condicionais: `.when('outro_campo', { is: ..., then: ..., otherwise: ... })` +- Exportar como named export: `export const minhaEntidadeSchema = object().shape({ ... })` +- Schemas em `formSchemas/` separados **não** precisam ser registrados em `formSchemas.js` — são importados diretamente na view + +### Componente CriarEditar.vue + +Local: `src/views//CriarEditar.vue` + +**Padrão com `useForm`** — formulários complexos (arrays dinâmicos, watches, valores computados): + +```vue + + + + + + +``` + +> **Atenção:** O padrão com `
` wrapper do vee-validate **não deve ser usado** neste projeto. Sempre use o padrão com `useForm` acima, independentemente da complexidade do formulário. + +### Componentes de campo (globais) + +| Componente | Uso | +| ----------------------------------------------------------------- | ------------------------------------------ | +| `` | Label com asterisco automático se required | +| `` | Input de texto padrão | +| `` | Select | +| `` | Mensagem de erro inline | +| `` | Textarea/input com contador de caracteres | +| `` | Datepicker | +| `` | Input numérico | +| `` | Autocomplete multi-select | +| `` | Array dinâmico de campos | +| `` | Lista todos os erros do formulário | +| `` | Botão salvar padronizado | +| `` | Cabeçalho com botão de fechar | + +### Classes CSS utilitárias + +- `flex g2` — flex com gap +- `f1` — flex: 1 (ocupa espaço disponível) +- `inputtext light` — estilo padrão de input +- `error-msg` — estilo de mensagem de erro diff --git a/frontend/CLAUDE.md b/frontend/CLAUDE.md new file mode 100644 index 000000000..61f689444 --- /dev/null +++ b/frontend/CLAUDE.md @@ -0,0 +1,6 @@ +# Frontend SMAE — Padrões de Desenvolvimento + +## Conhecimentos do Projeto + +- **Criação de formulários** (schemas Yup, componente CriarEditar, campos, padrões vee-validate): + [.claude/project-knowledge/form-creation.md](.claude/project-knowledge/form-creation.md) From 4feb01cd859aaa33d1be654346f0eceaa7777614 Mon Sep 17 00:00:00 2001 From: Gustavo Soares Date: Thu, 12 Mar 2026 17:09:13 -0300 Subject: [PATCH 02/12] feat: criando documentacao para SmaeTable --- frontend/src/components/SmaeTable/README.md | 191 ++++++++++++++++++++ 1 file changed, 191 insertions(+) create mode 100644 frontend/src/components/SmaeTable/README.md diff --git a/frontend/src/components/SmaeTable/README.md b/frontend/src/components/SmaeTable/README.md new file mode 100644 index 000000000..55f6d39c0 --- /dev/null +++ b/frontend/src/components/SmaeTable/README.md @@ -0,0 +1,191 @@ +# SmaeTable + +Componente de tabela padrão do SMAE. Substitui o uso de `` manual nas páginas de lista. + +**Caminho:** `@/components/SmaeTable/SmaeTable.vue` + +--- + +## Props + +| Prop | Tipo | Padrão | Descrição | +|---|---|---|---| +| `dados` | `Linha[]` | — | Array de objetos a renderizar (obrigatório) | +| `colunas` | `Coluna[]` | — | Definição das colunas (obrigatório) | +| `rotaEditar` | `string \| RouteLocationRaw \| (linha) => RouteLocationRaw` | — | Rota do botão editar. Função recebe a linha inteira. Retornar `false`/`null`/`undefined` oculta o botão. | +| `parametroDaRotaEditar` | `string` | `'id'` | Nome do parâmetro de rota | +| `parametroNoObjetoParaEditar` | `string` | `'id'` | Campo do objeto usado como valor do parâmetro | +| `esconderDeletar` | `boolean` | — | Oculta botão de excluir | +| `parametroNoObjetoParaExcluir` | `string` | `'descricao'` | Campo usado na mensagem de confirmação de exclusão | +| `mensagemExclusao` | `(linha) => string` | — | Função para mensagem customizada de confirmação | +| `titulo` | `string` | — | Caption da tabela | +| `schema` | `AnyObjectSchema` | — | Schema Yup para labels automáticos nos cabeçalhos | +| `replicarCabecalho` | `boolean` | — | Repete cabeçalho no rodapé | +| `rolagemHorizontal` | `boolean` | `false` | Envolve tabela em scroll horizontal | +| `tituloParaRolagemHorizontal` | `string` | — | Título para acessibilidade na rolagem horizontal (obrigatório se `rolagemHorizontal` e não há `titulo`) | +| `personalizarLinhas` | `{ parametro, alvo, classe }` | — | Aplica classe CSS condicional nas linhas | +| `subLinhaAbertaPorPadrao` | `boolean` | `false` | Sub-linhas expandidas por padrão | +| `subLinhaSempreVisivel` | `boolean` | `false` | Sub-linhas sempre visíveis (sem toggle) | +| `campoId` | `string` | `'id'` | Campo identificador (necessário para sub-linhas) | + +--- + +## Tipo `Coluna` + +```typescript +type Coluna = { + chave: string; // Acessa dado como linha[chave] (suporta notação ponto: 'obj.campo') + label?: string; // Texto do cabeçalho + ehCabecalho?: boolean; // Renderiza célula como
+ formatador?: (valor: unknown) => string | number; // Transforma o valor exibido + atributosDaCelula?: Record; + atributosDaColuna?: Record; + atributosDoCabecalhoDeColuna?: Record; + atributosDoRodapeDeColuna?: Record; + classe?: string | string[] | Record; +}; +``` + +--- + +## Evento + +| Evento | Payload | Descrição | +|---|---|---| +| `@deletar` | `linha: Linha` | Emitido após confirmação de exclusão. Recebe o objeto completo da linha. | + +--- + +## Slots + +| Slot | Props | Descrição | +|---|---|---| +| `#celula:` | `{ linha, celula }` | Renderização customizada de célula. Ex: `#celula:ativo` | +| `#cabecalho:` | `coluna` | Cabeçalho customizado de coluna | +| `#acoes="{ linha }"` | `{ linha, linhaIndex }` | Substitui os botões padrão de editar/excluir | +| `#sub-linha="{ linha, linhaIndex }"` | `{ linha, linhaIndex }` | Linha expansível (accordion) | +| `#corpo="{ dados }"` | `{ dados }` | Corpo inteiro customizado | +| `#rodape="{ colunas }"` | `colunas` | Rodapé customizado | +| `#titulo` | — | Caption customizado | + +O estado vazio é tratado automaticamente com a mensagem "Sem dados para exibir". + +--- + +## Exemplos de Uso + +### Mínimo + +```vue + +``` + +### rotaEditar condicional (permissão por item) + +```vue + +``` + +### Células customizadas + +```vue + + + +``` + +### Ações customizadas + +```vue + + + +``` + +### Sub-linhas expansíveis (accordion) + +```vue + + + +``` + +### Com loading separado + +```vue + + +``` + +--- + +## Exemplos Reais no Projeto + +| Arquivo | Características | +|---|---| +| [EtiquetasLista.vue](../../views/projetos.etiquetas/EtiquetasLista.vue) | Mínimo, `rotaEditar` condicional por `pode_editar` | +| [AreasTematicasLista.vue](../../views/areasTematicas/AreasTematicasLista.vue) | Slots de célula customizados, `LoadingComponent` separado | +| [AditivosLista.vue](../../views/tipoDeAditivo/AditivosLista.vue) | Com `FiltroParaPagina` e filtro client-side | From 8aee2cc1ca4867fb614fe37b7bcc8d8b56694d2b Mon Sep 17 00:00:00 2001 From: Gustavo Soares Date: Sat, 14 Mar 2026 15:34:25 -0300 Subject: [PATCH 03/12] =?UTF-8?q?feat:=20criando=20documenta=C3=A7=C3=A3o?= =?UTF-8?q?=20de=20cria=C3=A7=C3=A3o=20de=20lista?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../project-knowledge/list-creation.md | 453 ++++++++++++++++++ 1 file changed, 453 insertions(+) create mode 100644 frontend/.claude/project-knowledge/list-creation.md diff --git a/frontend/.claude/project-knowledge/list-creation.md b/frontend/.claude/project-knowledge/list-creation.md new file mode 100644 index 000000000..7269da117 --- /dev/null +++ b/frontend/.claude/project-knowledge/list-creation.md @@ -0,0 +1,453 @@ +# Criação de Listas no Frontend SMAE + +## 1. Visão Geral + +Páginas de lista seguem um padrão altamente consistente: + +- **Dados**: Pinia stores (`buscarTudo`, `excluirItem`) +- **Renderização**: Componente `SmaeTable` (substitui `` manual) +- **Cabeçalho**: Componente `CabecalhoDePagina` +- **Filtros**: Componente `FiltroParaPagina` (filtros com URL) ou `LocalFilter` (busca simples) +- **Loading**: Componente `LoadingComponent` +- **Estados vazio/erro**: Tratados automaticamente pelo `SmaeTable` + +--- + +## 2. Perguntas Obrigatórias Antes de Implementar + +Antes de criar uma lista, pergunte ao usuário: + +1. **A listagem tem filtros?** Se sim: quais campos? Os filtros devem persistir na URL? + - Com persistência na URL → `FiltroParaPagina` + - Busca simples client-side → `LocalFilter` (ou `filtrarObjetos` com `route.query`) + +2. **A listagem tem paginação?** Se sim, usar `MenuPaginacao`. Ver seção [MenuPaginacao](#8-menupaginacao). + +--- + +## 4. Convenção de Arquivos e Nomes + +``` +src/views// + Lista.vue # Página de listagem + Raiz.vue # Layout raiz (nested routes) + CriarEditar.vue # Formulário criar/editar +``` + +--- + +## 5. Template Mínimo de Lista (padrão atual) + +```vue + + + +``` + +--- + +## 5.1 Watch para Filtros e Paginação + +Se a lista tiver **filtros via URL** (`FiltroParaPagina` ou `route.query`), adicionar um `watch` que recarrega os dados quando os query params mudam: + +```javascript +import { onMounted, watch } from "vue"; +import { useRoute } from "vue-router"; + +const route = useRoute(); + +// Recarrega quando qualquer filtro muda +watch( + () => [ + route.query.campo1, + route.query.campo2, + ], + () => { + store.$reset(); + store.buscarTudo(route.query); + }, +); +``` + +Se tiver **paginação via `MenuPaginacao`**, usar `watchEffect` em vez de `watch` — ele rastreia automaticamente todos os `route.query` acessados dentro de `buscarTudo`: + +```javascript +import { watchEffect } from "vue"; + +// Rebusca ao mudar qualquer query param (filtros + pagina + token_paginacao) +watchEffect(() => { + store.buscarTudo(route.query); +}); +``` + +> **Regra:** filtros sem paginação → `watch` com array de campos explícitos. Com `MenuPaginacao` → `watchEffect` (rastreia tudo automaticamente). Nunca misturar os dois para o mesmo `buscarTudo`. + +--- + +## 6. SmaeTable + +**Caminho:** `@/components/SmaeTable/SmaeTable.vue` + +Ver documentação completa em [src/components/SmaeTable/README.md](../../src/components/SmaeTable/README.md). + +--- + +## 7. CabecalhoDePagina + +**Caminho:** `@/components/CabecalhoDePagina.vue` + +Substitui o `
` manual. Lê o título de `route.meta.título` automaticamente. + +```vue + + + +``` + +Slots disponíveis: `#titulo`, `#subtitulo`, `#acoes`. + +--- + +## 8. MenuPaginacao + +**Caminho:** `@/components/MenuPaginacao.vue` + +Ver documentação completa em [src/components/MenuPaginacao.md](../../src/components/MenuPaginacao.md). + +--- + +## 10. FiltroParaPagina + +**Caminho:** `@/components/FiltroParaPagina.vue` + +Ver documentação completa em [src/components/FiltroParaPagina.md](../../src/components/FiltroParaPagina.md). + +--- + +## 11. Pinia Store Padrão para Listas + +Stores **devem ser escritas em TypeScript** (`.store.ts`). + +```typescript +import { defineStore } from 'pinia'; + +const baseUrl = `${import.meta.env.VITE_API_URL}`; + +interface Item { + id: number; + descricao: string; + // adicionar campos conforme o DTO do backend +} + +interface Estado { + lista: Item[]; + emFoco: Item | null; + chamadasPendentes: { lista: boolean; emFoco: boolean }; + erro: null | unknown; +} + +export const useMinhaEntidadeStore = defineStore('minhaEntidade', { + state: (): Estado => ({ + lista: [], + emFoco: null, + chamadasPendentes: { lista: false, emFoco: false }, + erro: null, + }), + + actions: { + async buscarTudo(params = {}): Promise { + this.chamadasPendentes.lista = true; + this.erro = null; + try { + const { linhas } = await this.requestS.get(`${baseUrl}/minha-entidade`, params); + this.lista = linhas; + } catch (erro: unknown) { + this.erro = erro; + } + this.chamadasPendentes.lista = false; + }, + + async buscarItem(id: number): Promise { + this.chamadasPendentes.emFoco = true; + this.erro = null; + try { + this.emFoco = await this.requestS.get(`${baseUrl}/minha-entidade/${id}`); + } catch (erro: unknown) { + this.erro = erro; + } + this.chamadasPendentes.emFoco = false; + }, + + async salvarItem(params = {}, id = 0): Promise { + this.chamadasPendentes.emFoco = true; + this.erro = null; + try { + if (id) { + await this.requestS.patch(`${baseUrl}/minha-entidade/${id}`, params); + } else { + await this.requestS.post(`${baseUrl}/minha-entidade`, params); + } + this.chamadasPendentes.emFoco = false; + return true; + } catch (erro: unknown) { + this.erro = erro; + this.chamadasPendentes.emFoco = false; + return false; + } + }, + + async excluirItem(id: number): Promise { + this.chamadasPendentes.lista = true; + this.erro = null; + try { + await this.requestS.delete(`${baseUrl}/minha-entidade/${id}`); + this.chamadasPendentes.lista = false; + return true; + } catch (erro: unknown) { + this.erro = erro; + this.chamadasPendentes.lista = false; + return false; + } + }, + }, + + getters: { + itensPorId: ({ lista }): Record => + lista.reduce((acc, cur) => ({ ...acc, [cur.id]: cur }), {}), + }, +}); +``` + +### 11.1 Store com Paginação (MenuPaginacao) + +O componente `MenuPaginacao` recebe um objeto `paginacao` via `v-bind`. As chaves devem ser camelCase, correspondendo às props do componente: + +```typescript +// estado adicional +paginacao: { + paginas: 0, + temMais: false, + tokenPaginacao: '', + totalRegistros: 0, +}, + +// buscarTudo com paginação — mapear resposta da API para as props do MenuPaginacao +async buscarTudo(params = {}): Promise { + this.chamadasPendentes.lista = true; + this.erro = null; + try { + const { + linhas, + paginas, + tem_mais: temMais, + token_proxima_pagina: tokenPaginacao, + total: totalRegistros, + } = await this.requestS.get(`${baseUrl}/minha-entidade`, params); + + this.lista = linhas; + this.paginacao = { + paginas: paginas ?? 0, + temMais: temMais ?? false, + tokenPaginacao: tokenPaginacao ?? '', + totalRegistros: totalRegistros ?? 0, + }; + } catch (erro: unknown) { + this.erro = erro; + } + this.chamadasPendentes.lista = false; +}, +``` + +No template, usar `v-bind="paginacao"` — colocar acima e abaixo da tabela em listas longas: + +```vue + + + + + +``` + +No componente, reagir à mudança de página com `watchEffect` (em vez de `watch` com array de deps): + +```typescript +import { watchEffect } from 'vue'; +import { useRoute } from 'vue-router'; + +const route = useRoute(); + +// Rebusca automaticamente ao mudar pagina, token ou outros filtros na URL +watchEffect(() => { + store.buscarTudo(route.query); +}); +``` + +--- + +## 12. Configuração de Rotas + +```javascript +import MinhaEntidadeRaiz from "@/views/minhaEntidade/MinhaEntidadeRaiz.vue"; +import MinhaEntidadeLista from "@/views/minhaEntidade/MinhaEntidadeLista.vue"; +import MinhaEntidadeCriarEditar from "@/views/minhaEntidade/MinhaEntidadeCriarEditar.vue"; + +export default { + path: "minha-entidade", + component: MinhaEntidadeRaiz, + meta: { + título: "Minha Entidade", + limitarÀsPermissões: ["MinhaEntidade.listar"], + }, + children: [ + { + name: "minhaEntidade.listar", + path: "", + component: MinhaEntidadeLista, + meta: { título: "Minha Entidade" }, + }, + { + name: "minhaEntidade.criar", + path: "novo", + component: MinhaEntidadeCriarEditar, + meta: { + título: "Nova Entidade", + rotasParaMigalhasDePão: ["minhaEntidade.listar"], + rotaDeEscape: "minhaEntidade.listar", + }, + }, + { + name: "minhaEntidade.editar", + path: ":entidadeId", + component: MinhaEntidadeCriarEditar, + props: ({ params }) => ({ + ...params, + entidadeId: Number.parseInt(params.entidadeId, 10) || undefined, + }), + meta: { + título: "Editar Entidade", + rotasParaMigalhasDePão: ["minhaEntidade.listar"], + rotaDeEscape: "minhaEntidade.listar", + }, + }, + ], +}; +``` + +--- + +## 13. Ordenação Client-Side + +```javascript +import { computed } from "vue"; +import { useRoute, useRouter } from "vue-router"; + +const route = useRoute(); +const router = useRouter(); + +const parâmetroDeOrdenação = computed(() => + route.query.ordenar_por?.toLowerCase().trim(), +); +const ordemDeOrdenação = computed(() => + route.query.ordem?.toLowerCase().trim(), +); + +const listaOrdenada = computed(() => { + switch (parâmetroDeOrdenação.value) { + case "atualizado_em": + case "criado_em": + return ordemDeOrdenação.value === "decrescente" + ? lista.value.toSorted((a, b) => + a[parâmetroDeOrdenação.value] > b[parâmetroDeOrdenação.value] + ? -1 + : 1, + ) + : lista.value.toSorted((a, b) => + a[parâmetroDeOrdenação.value] > b[parâmetroDeOrdenação.value] + ? 1 + : -1, + ); + case "nome": + return ordemDeOrdenação.value === "decrescente" + ? lista.value.toSorted((a, b) => b.nome.localeCompare(a.nome)) + : lista.value.toSorted((a, b) => a.nome.localeCompare(b.nome)); + default: + return lista.value; + } +}); + +function aplicarOrdenação(nome, valor) { + router.replace({ query: { ...route.query, [nome]: valor || undefined } }); +} +``` + +--- + +## 14. Helpers Disponíveis + +| Helper | Caminho | Uso | +| ---------------- | -------------------------- | ------------------------------------------------------ | +| `filtrarObjetos` | `@/helpers/filtrarObjetos` | Filtra array por texto (recursivo, accent-insensitive) | +| `dateToField` | `@/helpers/dateToField` | Formata data para exibição | +| `dinheiro` | `@/helpers/dinheiro` | Formata valor monetário | +| `truncate` | `@/helpers/truncate` | Trunca texto longo | +| `removerHtml` | `@/helpers/removerHtml` | Remove tags HTML | + +--- + +## 15. Exemplos Reais no Projeto + +| Arquivo | Características | +| ------------------------------------------------------------------------------------------------------------ | ----------------------------------------------------------------------- | +| [EtiquetasLista.vue](src/views/projetos.etiquetas/EtiquetasLista.vue) | SmaeTable mínimo, rotaEditar condicional por `pode_editar` | +| [AreasTematicasLista.vue](src/views/areasTematicas/AreasTematicasLista.vue) | SmaeTable com slots de célula customizados, LoadingComponent separado | +| [AditivosLista.vue](src/views/tipoDeAditivo/AditivosLista.vue) | SmaeTable + FiltroParaPagina com busca client-side via `filtrarObjetos` | +| [TransferenciasVoluntariasLista.vue](src/views/transferenciasVoluntarias/TransferenciasVoluntariasLista.vue) | Filtros na URL, paginação "carregar mais" (ainda usa tabela manual) | +| [AcompanhamentosLista.vue](src/views/mdo.acompanhamentos/AcompanhamentosLista.vue) | Ordenação com URL query params (ainda usa tabela manual) | From da1024676cf2cac69b1774e2a3ef04723e97b0d6 Mon Sep 17 00:00:00 2001 From: Gustavo Soares Date: Sat, 14 Mar 2026 15:34:54 -0300 Subject: [PATCH 04/12] feat: documentando MenuPaginacao --- frontend/src/components/MenuPaginacao.md | 152 +++++++++++++++++++++++ 1 file changed, 152 insertions(+) create mode 100644 frontend/src/components/MenuPaginacao.md diff --git a/frontend/src/components/MenuPaginacao.md b/frontend/src/components/MenuPaginacao.md new file mode 100644 index 000000000..91080f996 --- /dev/null +++ b/frontend/src/components/MenuPaginacao.md @@ -0,0 +1,152 @@ +# MenuPaginacao + +Componente de paginação que sincroniza com a URL via query params. Só renderiza quando `paginas > 1`. + +**Caminho:** `@/components/MenuPaginacao.vue` + +--- + +## Props + +| Prop | Tipo | Padrão | Descrição | +|---|---|---|---| +| `paginas` | `number` | `0` | Total de páginas | +| `temMais` | `boolean` | `false` | Se há mais páginas a carregar | +| `tokenPaginacao` | `string` | `''` | Token da próxima página (repassado na query) | +| `totalRegistros` | `number` | `0` | Total de registros — exibido como texto ao lado da navegação | +| `prefixo` | `string` | `''` | Prefixo para as chaves na URL (útil quando há múltiplos paginadores na mesma página) | + +### v-model + +Aceita `v-model` para controle de página sem alterar a URL (uso em modais ou listas embutidas). + +--- + +## Eventos + +| Evento | Payload | Descrição | +|---|---|---| +| `trocaDePaginaSolicitada` | `{ pagina: number }` | Emitido ao navegar para qualquer página | + +--- + +## Comportamento + +- Sem `v-model`: navega via `router.push` atualizando `route.query.pagina` e `route.query.token_paginacao` +- Com `v-model`: atualiza o model diretamente, sem alterar a URL +- O componente **não é renderizado** quando `paginas <= 1` +- O token da próxima página é preservado na query para permitir navegação para frente + +--- + +## Uso com a URL (padrão) + +### 1. No template + +O padrão mais simples é passar o objeto `paginacao` da store com `v-bind`. Colocar **acima e abaixo** da tabela em listas longas: + +```vue + + + + + +``` + +### 2. Na store (TypeScript) + +O objeto `paginacao` deve ter as mesmas chaves das props (camelCase): + +```typescript +interface Paginacao { + paginas: number; + temMais: boolean; + tokenPaginacao: string; + totalRegistros: number; +} + +interface Estado { + lista: Item[]; + paginacao: Paginacao; + chamadasPendentes: { lista: boolean }; + erro: null | unknown; +} + +// estado inicial +paginacao: { + paginas: 0, + temMais: false, + tokenPaginacao: '', + totalRegistros: 0, +}, + +// em buscarTudo, mapear a resposta da API +async buscarTudo(params = {}): Promise { + this.chamadasPendentes.lista = true; + this.erro = null; + try { + const { + linhas, + paginas, + tem_mais: temMais, + token_proxima_pagina: tokenPaginacao, + total: totalRegistros, + } = await this.requestS.get(`${baseUrl}/minha-entidade`, params); + + this.lista = linhas; + this.paginacao = { + paginas: paginas ?? 0, + temMais: temMais ?? false, + tokenPaginacao: tokenPaginacao ?? '', + totalRegistros: totalRegistros ?? 0, + }; + } catch (erro: unknown) { + this.erro = erro; + } + this.chamadasPendentes.lista = false; +}, +``` + +### 3. No componente: reagir à mudança de página + +```typescript +import { watchEffect } from 'vue'; +import { useRoute } from 'vue-router'; + +const route = useRoute(); + +// Rebusca automaticamente ao mudar pagina, token ou outros filtros na URL +watchEffect(() => { + store.buscarTudo(route.query); +}); +``` + +--- + +## Com prefixo (múltiplos paginadores na mesma página) + +```vue + + +``` + +Com `prefixo="obras_"`, usa `route.query.obras_pagina` e `route.query.obras_token_paginacao`. + +Ao usar junto com `FiltroParaPagina`, passar o mesmo prefixo para que o filtro resets a paginação correta: + +```vue + +``` + +--- + +## Exemplos Reais no Projeto + +| Arquivo | Características | +|---|---| +| [ObrasListar.vue](../views/mdo.obras/ObrasListar.vue) | `v-bind="paginacao"`, paginador acima e abaixo da tabela | +| [ComunicadosGeraisLista.vue](../views/comunicadosGerais/ComunicadosGeraisLista.vue) | Com fallback "Buscar mais" quando `temMais` | From 5e19c1f2d5dd81036ce0130ad9bacf2bcdca6521 Mon Sep 17 00:00:00 2001 From: Gustavo Soares Date: Sat, 14 Mar 2026 15:53:14 -0300 Subject: [PATCH 05/12] feat: criando configuracao basica para criacao de rota --- .../project-knowledge/route-creation.md | 239 ++++++++++++++++++ 1 file changed, 239 insertions(+) create mode 100644 frontend/.claude/project-knowledge/route-creation.md diff --git a/frontend/.claude/project-knowledge/route-creation.md b/frontend/.claude/project-knowledge/route-creation.md new file mode 100644 index 000000000..155dd96fc --- /dev/null +++ b/frontend/.claude/project-knowledge/route-creation.md @@ -0,0 +1,239 @@ +# Criação de Rotas + +## Estrutura de Arquivos + +Cada módulo tem seu próprio arquivo de rotas em `src/router/`. O arquivo principal `src/router/index.js` importa e registra todos os módulos. + +``` +src/router/ +├── index.js # router principal +├── helpers/ +│ └── tiparPropsDeRota.ts +├── administracao.js +├── configuracoes.js +├── metas.js +└── [outros módulos].js +``` + +Para criar rotas de um novo módulo, crie `src/router/meu-modulo.js` e registre no `index.js`. + +--- + +## Estrutura Básica de uma Rota + +```js +{ + path: '/meu-modulo', + component: MeuModuloRaiz, + name: 'meuModuloRaiz', + meta: { + título: 'Meu Módulo', + entidadeMãe: 'pdm', // contexto do módulo + limitarÀsPermissões: ['Modulo.permissao'], + íconeParaMenu: `...`, + rotasParaMenuPrincipal: ['entidadeMae.meuModulo.listar'], + }, + children: [ + { + name: 'entidadeMae.meuModulo.listar', + path: '', + component: MeuModuloLista, + meta: { título: 'Meu Módulo' }, + }, + { + name: 'entidadeMae.meuModulo.criar', + path: 'novo', + component: MeuModuloCriarEditar, + meta: { + título: 'Novo item', + rotaDeEscape: 'entidadeMae.meuModulo.listar', + rotasParaMigalhasDePão: ['entidadeMae.meuModulo.listar'], + }, + }, + { + path: ':itemId', + name: 'entidadeMae.meuModulo.editar', + component: MeuModuloCriarEditar, + props: ({ params }) => ({ + ...params, + itemId: Number.parseInt(params.itemId, 10) || undefined, + }), + meta: { + título: 'Editar item', + rotaDeEscape: 'entidadeMae.meuModulo.listar', + rotasParaMigalhasDePão: ['entidadeMae.meuModulo.listar'], + }, + }, + ], +} +``` + +--- + +## Props de Rota + +### `tiparPropsDeRota` — uso padrão + +Sempre que os parâmetros da rota precisam ser passados como props para o componente, use `tiparPropsDeRota`: + +```js +import tiparPropsDeRota from '@/router/helpers/tiparPropsDeRota'; + +{ + path: 'demandas/:id', + name: 'demandaDetalhe', + component: DemandaDetalhe, + props: tiparPropsDeRota, +} +``` + +A função converte automaticamente os parâmetros para os tipos corretos (string, boolean, number) e decodifica primitivas. + +### Props com conversão manual + +Quando precisar forçar um tipo específico: + +```js +props: ({ params }) => ({ + ...params, + portfolioId: Number.parseInt(params.portfolioId, 10) || undefined, +}), +``` + +### Props estáticas + +```js +props: { type: 'novo', parentPage: 'metas' }, +``` + +### Props de query e params juntos + +```js +props: ({ params, query }) => ({ + ...params, + ...query, + opcao: Number(query.opcao), +}), +``` + +--- + +## Campos `meta` Comuns + +| Campo | Tipo | Descrição | +|---|---|---| +| `título` | `string \| Function` | Título da página. Pode ser uma função que lê de uma store Pinia | +| `limitarÀsPermissões` | `string[]` | Permissões necessárias para acessar a rota | +| `entidadeMãe` | `string` | Contexto do módulo: `'pdm'`, `'projeto'`, `'mdo'`, `'planoSetorial'` | +| `rotaDeEscape` | `string` | Nome da rota para onde ir ao cancelar/voltar | +| `rotasParaMigalhasDePão` | `string[]` | Nomes de rotas que formam o breadcrumb | +| `rotasParaMenuPrincipal` | `string[]` | Nomes de rotas exibidas no menu lateral principal | +| `rotasParaMenuSecundário` | `string[] \| Function` | Nomes de rotas exibidas no menu secundário | +| `íconeParaMenu` | `string` | SVG inline para exibir no menu | +| `tituloParaMigalhaDePao` | `string \| Function` | Texto customizado para o breadcrumb (se diferente de `título`) | +| `rotaPrescindeDeChave` | `boolean` | A rota não exige uma entidade selecionada (ex: lista raiz) | +| `publico` | `boolean` | Acessível sem autenticação | + +### Título dinâmico com Pinia + +```js +meta: { + título: () => { + const meta = useCiclosStore()?.SingleMeta?.meta; + return meta?.código && meta?.titulo + ? `Meta ${meta.código} ${meta.titulo}` + : 'Meta'; + }, +} +``` + +--- + +## Importação de Componentes + +### Lazy loading (padrão para views) + +```js +component: () => import('@/views/meu-modulo/MeuModuloLista.vue'), +``` + +### Eager loading (views pequenas ou muito usadas) + +```js +import MeuModuloRaiz from '@/views/meu-modulo/MeuModuloRaiz.vue'; +// ... +component: MeuModuloRaiz, +``` + +### Com loading component + +```js +import { defineAsyncComponent } from 'vue'; +import LoadingComponent from '@/components/LoadingComponent.vue'; + +const MeuModuloLista = defineAsyncComponent({ + loader: () => import('@/views/meu-modulo/MeuModuloLista.vue'), + loadingComponent: LoadingComponent, +}); +``` + +--- + +## Registrando o Módulo no Router Principal + +Em `src/router/index.js`, importe e adicione à lista de rotas: + +```js +// import direto (objeto único) +import meuModulo from './meu-modulo.js'; + +// ou spread (array de rotas) +import * as meuModulo from './meu-modulo.js'; + +const routes = [ + // ...outras rotas + meuModulo, // import direto + ...meuModulo, // spread +]; +``` + +--- + +## Rota Pública (sem autenticação) + +```js +{ + path: '/publico', + component: PublicLayout, + meta: { publico: true }, + children: [ + { + path: 'item/:id', + name: 'itemPublico', + component: ItemPublicoDetalhe, + props: tiparPropsDeRota, + meta: { título: 'Detalhe do Item' }, + }, + ], +} +``` + +--- + +## Redirect + +```js +{ + path: '/modulo', + redirect: { name: 'moduloListar' }, + children: [...] +} +``` + +--- + +## Convenções de Nomenclatura + +- Nomes de rotas: camelCase seguindo o padrão `módulo + Ação` → `portfolioListar`, `portfolioCriar`, `portfolioEditar` +- Paths: kebab-case → `/meu-modulo/novo` +- Params de ID: sempre `nomeEntidadeId` → `:portfolioId`, `:metaId` \ No newline at end of file From 7cb13cfd53e740cffc5bcfe3079db00c832819ef Mon Sep 17 00:00:00 2001 From: Gustavo Soares Date: Sat, 14 Mar 2026 15:53:37 -0300 Subject: [PATCH 06/12] feat: separando arquivo para criacao do store --- .../project-knowledge/list-creation.md | 158 +------------- .../project-knowledge/store-creation.md | 195 ++++++++++++++++++ 2 files changed, 197 insertions(+), 156 deletions(-) create mode 100644 frontend/.claude/project-knowledge/store-creation.md diff --git a/frontend/.claude/project-knowledge/list-creation.md b/frontend/.claude/project-knowledge/list-creation.md index 7269da117..8e8e38659 100644 --- a/frontend/.claude/project-knowledge/list-creation.md +++ b/frontend/.claude/project-knowledge/list-creation.md @@ -172,163 +172,9 @@ Ver documentação completa em [src/components/FiltroParaPagina.md](../../src/co --- -## 11. Pinia Store Padrão para Listas +## 11. Pinia Store -Stores **devem ser escritas em TypeScript** (`.store.ts`). - -```typescript -import { defineStore } from 'pinia'; - -const baseUrl = `${import.meta.env.VITE_API_URL}`; - -interface Item { - id: number; - descricao: string; - // adicionar campos conforme o DTO do backend -} - -interface Estado { - lista: Item[]; - emFoco: Item | null; - chamadasPendentes: { lista: boolean; emFoco: boolean }; - erro: null | unknown; -} - -export const useMinhaEntidadeStore = defineStore('minhaEntidade', { - state: (): Estado => ({ - lista: [], - emFoco: null, - chamadasPendentes: { lista: false, emFoco: false }, - erro: null, - }), - - actions: { - async buscarTudo(params = {}): Promise { - this.chamadasPendentes.lista = true; - this.erro = null; - try { - const { linhas } = await this.requestS.get(`${baseUrl}/minha-entidade`, params); - this.lista = linhas; - } catch (erro: unknown) { - this.erro = erro; - } - this.chamadasPendentes.lista = false; - }, - - async buscarItem(id: number): Promise { - this.chamadasPendentes.emFoco = true; - this.erro = null; - try { - this.emFoco = await this.requestS.get(`${baseUrl}/minha-entidade/${id}`); - } catch (erro: unknown) { - this.erro = erro; - } - this.chamadasPendentes.emFoco = false; - }, - - async salvarItem(params = {}, id = 0): Promise { - this.chamadasPendentes.emFoco = true; - this.erro = null; - try { - if (id) { - await this.requestS.patch(`${baseUrl}/minha-entidade/${id}`, params); - } else { - await this.requestS.post(`${baseUrl}/minha-entidade`, params); - } - this.chamadasPendentes.emFoco = false; - return true; - } catch (erro: unknown) { - this.erro = erro; - this.chamadasPendentes.emFoco = false; - return false; - } - }, - - async excluirItem(id: number): Promise { - this.chamadasPendentes.lista = true; - this.erro = null; - try { - await this.requestS.delete(`${baseUrl}/minha-entidade/${id}`); - this.chamadasPendentes.lista = false; - return true; - } catch (erro: unknown) { - this.erro = erro; - this.chamadasPendentes.lista = false; - return false; - } - }, - }, - - getters: { - itensPorId: ({ lista }): Record => - lista.reduce((acc, cur) => ({ ...acc, [cur.id]: cur }), {}), - }, -}); -``` - -### 11.1 Store com Paginação (MenuPaginacao) - -O componente `MenuPaginacao` recebe um objeto `paginacao` via `v-bind`. As chaves devem ser camelCase, correspondendo às props do componente: - -```typescript -// estado adicional -paginacao: { - paginas: 0, - temMais: false, - tokenPaginacao: '', - totalRegistros: 0, -}, - -// buscarTudo com paginação — mapear resposta da API para as props do MenuPaginacao -async buscarTudo(params = {}): Promise { - this.chamadasPendentes.lista = true; - this.erro = null; - try { - const { - linhas, - paginas, - tem_mais: temMais, - token_proxima_pagina: tokenPaginacao, - total: totalRegistros, - } = await this.requestS.get(`${baseUrl}/minha-entidade`, params); - - this.lista = linhas; - this.paginacao = { - paginas: paginas ?? 0, - temMais: temMais ?? false, - tokenPaginacao: tokenPaginacao ?? '', - totalRegistros: totalRegistros ?? 0, - }; - } catch (erro: unknown) { - this.erro = erro; - } - this.chamadasPendentes.lista = false; -}, -``` - -No template, usar `v-bind="paginacao"` — colocar acima e abaixo da tabela em listas longas: - -```vue - - - - - -``` - -No componente, reagir à mudança de página com `watchEffect` (em vez de `watch` com array de deps): - -```typescript -import { watchEffect } from 'vue'; -import { useRoute } from 'vue-router'; - -const route = useRoute(); - -// Rebusca automaticamente ao mudar pagina, token ou outros filtros na URL -watchEffect(() => { - store.buscarTudo(route.query); -}); -``` +Ver documentação completa em [store-creation.md](store-creation.md). --- diff --git a/frontend/.claude/project-knowledge/store-creation.md b/frontend/.claude/project-knowledge/store-creation.md new file mode 100644 index 000000000..4c453120d --- /dev/null +++ b/frontend/.claude/project-knowledge/store-creation.md @@ -0,0 +1,195 @@ +# Criação de Pinia Store + +## Convenção de Arquivos + +Stores ficam em `src/stores/.store.ts` e **devem ser escritas em TypeScript**. + +--- + +## Store Padrão (sem paginação) + +```typescript +import { defineStore } from 'pinia'; + +const baseUrl = `${import.meta.env.VITE_API_URL}`; + +interface Item { + id: number; + descricao: string; + // adicionar campos conforme o DTO do backend +} + +interface Estado { + lista: Item[]; + emFoco: Item | null; + chamadasPendentes: { lista: boolean; emFoco: boolean }; + erro: null | unknown; +} + +export const useMinhaEntidadeStore = defineStore('minhaEntidade', { + state: (): Estado => ({ + lista: [], + emFoco: null, + chamadasPendentes: { lista: false, emFoco: false }, + erro: null, + }), + + actions: { + async buscarTudo(params = {}): Promise { + this.chamadasPendentes.lista = true; + this.erro = null; + try { + const { linhas } = await this.requestS.get(`${baseUrl}/minha-entidade`, params); + this.lista = linhas; + } catch (erro: unknown) { + this.erro = erro; + } + this.chamadasPendentes.lista = false; + }, + + async buscarItem(id: number): Promise { + this.chamadasPendentes.emFoco = true; + this.erro = null; + try { + this.emFoco = await this.requestS.get(`${baseUrl}/minha-entidade/${id}`); + } catch (erro: unknown) { + this.erro = erro; + } + this.chamadasPendentes.emFoco = false; + }, + + async salvarItem(params = {}, id = 0): Promise { + this.chamadasPendentes.emFoco = true; + this.erro = null; + try { + if (id) { + await this.requestS.patch(`${baseUrl}/minha-entidade/${id}`, params); + } else { + await this.requestS.post(`${baseUrl}/minha-entidade`, params); + } + this.chamadasPendentes.emFoco = false; + return true; + } catch (erro: unknown) { + this.erro = erro; + this.chamadasPendentes.emFoco = false; + return false; + } + }, + + async excluirItem(id: number): Promise { + this.chamadasPendentes.lista = true; + this.erro = null; + try { + await this.requestS.delete(`${baseUrl}/minha-entidade/${id}`); + this.chamadasPendentes.lista = false; + return true; + } catch (erro: unknown) { + this.erro = erro; + this.chamadasPendentes.lista = false; + return false; + } + }, + }, + + getters: { + itensPorId: ({ lista }): Record => + lista.reduce((acc, cur) => ({ ...acc, [cur.id]: cur }), {}), + }, +}); +``` + +--- + +## Store com Paginação (MenuPaginacao) + +Adicionar `paginacao` ao estado e mapear a resposta da API para as props do `MenuPaginacao` (camelCase): + +```typescript +interface Estado { + lista: Item[]; + emFoco: Item | null; + chamadasPendentes: { lista: boolean; emFoco: boolean }; + erro: null | unknown; + paginacao: { + paginas: number; + temMais: boolean; + tokenPaginacao: string; + totalRegistros: number; + }; +} + +// estado inicial +state: (): Estado => ({ + lista: [], + emFoco: null, + chamadasPendentes: { lista: false, emFoco: false }, + erro: null, + paginacao: { + paginas: 0, + temMais: false, + tokenPaginacao: '', + totalRegistros: 0, + }, +}), +``` + +`buscarTudo` com paginação — mapear snake_case da API para camelCase: + +```typescript +async buscarTudo(params = {}): Promise { + this.chamadasPendentes.lista = true; + this.erro = null; + try { + const { + linhas, + paginas, + tem_mais: temMais, + token_proxima_pagina: tokenPaginacao, + total: totalRegistros, + } = await this.requestS.get(`${baseUrl}/minha-entidade`, params); + + this.lista = linhas; + this.paginacao = { + paginas: paginas ?? 0, + temMais: temMais ?? false, + tokenPaginacao: tokenPaginacao ?? '', + totalRegistros: totalRegistros ?? 0, + }; + } catch (erro: unknown) { + this.erro = erro; + } + this.chamadasPendentes.lista = false; +}, +``` + +No componente de lista, usar `watchEffect` (rastreia automaticamente todos os `route.query` acessados dentro de `buscarTudo`): + +```typescript +import { watchEffect } from 'vue'; +import { useRoute } from 'vue-router'; + +const route = useRoute(); + +watchEffect(() => { + store.buscarTudo(route.query); +}); +``` + +No template, usar `v-bind="paginacao"` — colocar acima e abaixo da tabela: + +```vue + + + +``` + +--- + +## Regra watch vs watchEffect + +| Situação | Hook | +|---|---| +| Filtros sem paginação | `watch` com array explícito de `route.query` campos | +| Com `MenuPaginacao` | `watchEffect` (rastreia tudo automaticamente) | + +Nunca misturar os dois para o mesmo `buscarTudo`. From a8bb01dbc773c4a4fd8c5095b104d3df48c00590 Mon Sep 17 00:00:00 2001 From: Gustavo Soares Date: Sat, 14 Mar 2026 15:54:08 -0300 Subject: [PATCH 07/12] feat: criando skill para novo formulario --- frontend/.claude/skills/novo-formulario.md | 35 ++++++++++++++++++++++ frontend/CLAUDE.md | 9 ++++++ 2 files changed, 44 insertions(+) create mode 100644 frontend/.claude/skills/novo-formulario.md diff --git a/frontend/.claude/skills/novo-formulario.md b/frontend/.claude/skills/novo-formulario.md new file mode 100644 index 000000000..1e0ce37f0 --- /dev/null +++ b/frontend/.claude/skills/novo-formulario.md @@ -0,0 +1,35 @@ +--- +allowed-tools: Read, Write, Edit, Glob, Grep, Bash +description: Cria uma novas paginas de formulario (schema Yup + componente Vue CriarEditar) +--- + +## Sua tarefa + +$ARGUMENTS + +Antes de começar, leia os seguintes arquivos de conhecimento do projeto: + +- `.claude/project-knowledge/form-creation.md` — padrões de schema Yup e componente CriarEditar +- `.claude/project-knowledge/list-creation.md` — padrão de lista (SmaeTable, paginação, filtros) +- `.claude/project-knowledge/store-creation.md` — padrão de store Pinia (estado, actions, paginação) +- `.claude/project-knowledge/route-creation.md` — padrão de rotas (meta campos, props, lazy loading, permissões) + +Se os argumentos não especificarem detalhes suficientes, pergunte: + +1. **Nome da entidade** (ex: "Parceiro", "TipoDeAcao") +2. **Módulo/pasta** onde ficará em `src/views/` (ex: `parceiros`, `tiposDeAcao`) +3. **Campos do formulário**: nome, tipo (texto, número, data, select, autocomplete, boolean), obrigatório, label em português, tamanho máximo se aplicável +4. **Tem lista de itens dinâmica (FieldArray)?** Se sim, quais campos +5. **Nome da store** (ex: `useParceirosStore`) +6. **Nome da rota de listagem** para redirecionar após salvar (ex: `parceiros.listar`) +7. **A lista tem filtros?** Se sim: quais campos? Os filtros devem persistir na URL? +8. **A lista tem paginação?** (MenuPaginacao) + +Depois de coletar as informações, crie os seguintes arquivos seguindo os padrões dos documentos lidos: + +1. `src/consts/formSchemas/.ts` +2. `src/stores/.store.ts` +3. `src/views//Raiz.vue` +4. `src/views//Lista.vue` +5. `src/views//CriarEditar.vue` +6. Bloco de configuração de rota (para adicionar no arquivo de rotas do módulo) diff --git a/frontend/CLAUDE.md b/frontend/CLAUDE.md index 61f689444..97db40391 100644 --- a/frontend/CLAUDE.md +++ b/frontend/CLAUDE.md @@ -4,3 +4,12 @@ - **Criação de formulários** (schemas Yup, componente CriarEditar, campos, padrões vee-validate): [.claude/project-knowledge/form-creation.md](.claude/project-knowledge/form-creation.md) + +- **Criação de listas** (stores Pinia, tabelas, LocalFilter, filtros, ordenação, paginação, ações): + [.claude/project-knowledge/list-creation.md](.claude/project-knowledge/list-creation.md) + +- **Criação de rotas** (estrutura de arquivo, meta campos, props, lazy loading, permissões, breadcrumbs): + [.claude/project-knowledge/route-creation.md](.claude/project-knowledge/route-creation.md) + +- **Criação de stores Pinia** (estado, actions, paginação, watch vs watchEffect): + [.claude/project-knowledge/store-creation.md](.claude/project-knowledge/store-creation.md) From 060008a76366e58dc246bebbede1b4fda0d8eccc Mon Sep 17 00:00:00 2001 From: Gustavo Soares Date: Mon, 16 Mar 2026 10:59:29 -0300 Subject: [PATCH 08/12] feat: ajustando detalhes documentacao --- .../project-knowledge/form-creation.md | 56 ------------------- .../project-knowledge/list-creation.md | 32 +++++------ .../project-knowledge/route-creation.md | 20 ++----- 3 files changed, 19 insertions(+), 89 deletions(-) diff --git a/frontend/.claude/project-knowledge/form-creation.md b/frontend/.claude/project-knowledge/form-creation.md index 83ae61da7..9a8173db2 100644 --- a/frontend/.claude/project-knowledge/form-creation.md +++ b/frontend/.claude/project-knowledge/form-creation.md @@ -75,62 +75,6 @@ watch(emFoco, (novosValores) => { }); - - -