docs: CLAUDE.md com contexto completo do projeto para desenvolvimento assistido por IA
Cobre: stack, arquitetura multi-tenant, ordem de inicialização, theming dinâmico, dark mode, clientes HTTP, estrutura de pastas, rotas, fluxo de login, variáveis de ambiente, regras de código, acessibilidade, como adicionar telas, pendências e pitfalls. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
1612b89867
commit
d234b9ebe0
299
CLAUDE.md
Normal file
299
CLAUDE.md
Normal file
@ -0,0 +1,299 @@
|
|||||||
|
# portal-modumfiscal-web — Contexto para IA
|
||||||
|
|
||||||
|
Portal público de autoatendimento fiscal do contribuinte. SaaS multi-tenant: cada
|
||||||
|
prefeitura cliente tem seu próprio subdomínio (`tutoia.modumfiscal.com.br`) e uma
|
||||||
|
identidade visual distinta (cor primária + foto de fundo + logo).
|
||||||
|
|
||||||
|
Projeto irmão: `core-modumfiscal-web` (backoffice interno da prefeitura, outra repo).
|
||||||
|
Backend compartilhado: API REST Spring Boot em `https://sistema.modumfiscal.com.br`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Stack
|
||||||
|
|
||||||
|
| Dependência | Versão | Observação |
|
||||||
|
|---|---|---|
|
||||||
|
| Vue | 3.5 | `<script setup>` obrigatório |
|
||||||
|
| Vite | 8 | Plugin `@tailwindcss/vite` (sem PostCSS) |
|
||||||
|
| PrimeVue | 4.5 | Tema Aura, auto-importado via `unplugin-vue-components` |
|
||||||
|
| `@primeuix/themes` | 2 | `updatePreset`, `updateSurfacePalette`, `definePreset` |
|
||||||
|
| TailwindCSS | 4 | `@import "tailwindcss"` no CSS — **sem `tailwind.config.js`** |
|
||||||
|
| Vue Router | 5 | `createWebHistory` |
|
||||||
|
| Pinia | 3 | `pinia-plugin-persistedstate` |
|
||||||
|
| Axios | 1.x | Dois clientes: público e autenticado |
|
||||||
|
| Zod | 4 | Validação de formulários (usar nos wizards) |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Arquitetura Multi-Tenant
|
||||||
|
|
||||||
|
```
|
||||||
|
subdomínio (ex: tutoia.modumfiscal.com.br)
|
||||||
|
└── getTenant() → 'tutoia'
|
||||||
|
└── bootstrapPrefeitura() → GET /api/v1/publico/prefeitura/tutoia
|
||||||
|
└── prefeituraStore.$patch({ nomePrefeitura, template, pathLogo, ... })
|
||||||
|
└── App.vue onMounted → applyTemplate('tutoia')
|
||||||
|
└── PrimeVue recebe cor primária laranja (tutoia)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Ordem de inicialização em `main.js` (crítica — não alterar):**
|
||||||
|
1. `createPinia()` + `pinia.use(persistedstate)`
|
||||||
|
2. `await bootstrapPrefeitura(pinia)` — busca config da prefeitura antes de montar
|
||||||
|
3. `app.use(router)`
|
||||||
|
4. `app.use(PrimeVue, primeVueConfig)`
|
||||||
|
5. `app.mount('#app')`
|
||||||
|
6. `App.vue onMounted` → `applyTemplate(prefeitura.template)` — aplica cor dinâmica
|
||||||
|
|
||||||
|
> **Por que `applyTemplate` fica no `onMounted` e não no bootstrap?**
|
||||||
|
> `updatePreset()` exige que o PrimeVue já esteja inicializado. No bootstrap,
|
||||||
|
> o `app.use(PrimeVue)` ainda não rodou. Chamá-la antes causa erro silencioso.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Detecção de Tenant
|
||||||
|
|
||||||
|
```js
|
||||||
|
// src/utils/tenant.js
|
||||||
|
getTenant() → hostname.split('.')[0]
|
||||||
|
→ localStorage('current_dominio') // fallback
|
||||||
|
→ 'sistema' // último fallback
|
||||||
|
```
|
||||||
|
|
||||||
|
`localhost` é inválido propositalmente — em dev, o bootstrap falha graciosamente
|
||||||
|
e o app sobe sem tema personalizado (usa `blue` como padrão).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Theming Dinâmico
|
||||||
|
|
||||||
|
Templates disponíveis em `src/config/theme.config.js`:
|
||||||
|
`sistema · tutoia · amber · blue · indigo · violet · emerald · teal · rose`
|
||||||
|
|
||||||
|
O campo `template` retornado pela API (ex: `"tutoia"`) determina a paleta primary.
|
||||||
|
A superfície é sempre `slate` — não mudar.
|
||||||
|
|
||||||
|
Para adicionar novo município com foto de fundo:
|
||||||
|
1. Colocar a imagem em `src/assets/images/bg-NOME.jpeg`
|
||||||
|
2. Importar em `HomeView.vue` e adicionar ao `heroBgMap`
|
||||||
|
3. Cadastrar o `template` correspondente em `theme.config.js` se precisar de cor nova
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Dark Mode
|
||||||
|
|
||||||
|
- Classe `.app-dark` no `<html>` ativa o dark mode
|
||||||
|
- PrimeVue: `darkModeSelector: '.app-dark'` em `primevue.config.js`
|
||||||
|
- Tailwind: `@variant dark (&:where(.app-dark, .app-dark *))` em `main.css`
|
||||||
|
- O widget `AccessibilityWidget.vue` gerencia a classe e persiste no `localStorage`
|
||||||
|
|
||||||
|
**Toda view nova deve incluir `dark:` nos utilitários Tailwind.** Referência rápida:
|
||||||
|
|
||||||
|
| Light | Dark |
|
||||||
|
|---|---|
|
||||||
|
| `bg-white` | `dark:bg-slate-900` |
|
||||||
|
| `bg-slate-50` | `dark:bg-slate-950` |
|
||||||
|
| `bg-slate-100` | `dark:bg-slate-800` |
|
||||||
|
| `border-slate-200` | `dark:border-slate-700` |
|
||||||
|
| `text-slate-800` | `dark:text-slate-100` |
|
||||||
|
| `text-slate-600` | `dark:text-slate-300` |
|
||||||
|
| `text-slate-500` | `dark:text-slate-400` |
|
||||||
|
| `hover:bg-slate-100` | `dark:hover:bg-slate-800` |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Clientes HTTP
|
||||||
|
|
||||||
|
```js
|
||||||
|
// src/config/apiClient.js
|
||||||
|
apiClientPublico // sem Authorization — para bootstrap e serviços públicos
|
||||||
|
apiClient // com Bearer token + headers tenant — para /portal/*
|
||||||
|
```
|
||||||
|
|
||||||
|
Interceptors adicionam automaticamente `X-Municipio` e `X-Dominio` em toda requisição.
|
||||||
|
`apiClient` redireciona para `/` em resposta 401.
|
||||||
|
|
||||||
|
**Regra:** nunca chamar `apiClient` diretamente em componente — sempre via Service.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Estrutura de Pastas
|
||||||
|
|
||||||
|
```
|
||||||
|
src/
|
||||||
|
├── assets/
|
||||||
|
│ ├── main.css ← Tailwind v4 directives + @variant dark
|
||||||
|
│ ├── layout/layout.scss ← Focus ring, skip link, prefers-reduced-motion, classes a11y
|
||||||
|
│ └── images/ ← Logos e fotos de fundo (importação estática Vite)
|
||||||
|
├── bootstrap/
|
||||||
|
│ └── prefeituraBoot.js ← Busca config da prefeitura antes de montar o app
|
||||||
|
├── components/
|
||||||
|
│ ├── common/
|
||||||
|
│ │ ├── AppHeader.vue ← Header público (logo dinâmica, nav)
|
||||||
|
│ │ ├── AppFooter.vue ← Footer com links institucionais
|
||||||
|
│ │ ├── ServiceCard.vue ← Card reutilizável de serviço (público e autenticado)
|
||||||
|
│ │ └── AccessibilityWidget.vue ← Botão flutuante: fonte, dark, alto contraste
|
||||||
|
│ └── auth/
|
||||||
|
│ └── DocumentoInput.vue ← Input CPF/CNPJ com máscara automática
|
||||||
|
├── composables/
|
||||||
|
│ └── useMotion.js ← prefers-reduced-motion reativo
|
||||||
|
├── config/
|
||||||
|
│ ├── apiClient.js ← Axios clients (público + autenticado)
|
||||||
|
│ ├── primevue.config.js ← Aura preset + locale pt-BR
|
||||||
|
│ └── theme.config.js ← Paletas e applyTemplate()
|
||||||
|
├── layouts/
|
||||||
|
│ ├── PublicLayout.vue ← Header + Footer + RouterView (rotas públicas)
|
||||||
|
│ └── PortalLayout.vue ← Header autenticado com nav + Sair
|
||||||
|
├── router/index.js ← Rotas + guard requiresAuth
|
||||||
|
├── services/
|
||||||
|
│ ├── prefeituraService.js ← GET /publico/prefeitura/{dominio}
|
||||||
|
│ └── authService.js ← Keycloak PKCE (estrutura pronta, integração pendente)
|
||||||
|
├── stores/
|
||||||
|
│ ├── prefeituraStore.js ← Config da prefeitura (persiste localStorage)
|
||||||
|
│ └── authStore.js ← Token + userInfo (persiste localStorage)
|
||||||
|
├── utils/
|
||||||
|
│ └── tenant.js ← getTenant / setTenant / clearTenant
|
||||||
|
└── views/
|
||||||
|
├── public/ ← Home, Login, PrimeiroAcesso, Credenciamento
|
||||||
|
├── servicos/ ← ServicosHub, Certidao, Iptu
|
||||||
|
└── portal/ ← Painel, Debitos, Certidoes, Alvaras, Pagamentos, Dados
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Rotas
|
||||||
|
|
||||||
|
| Path | Nome | Layout | Auth |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `/` | `home` | Public | ✗ |
|
||||||
|
| `/login` | `login` | Public | ✗ |
|
||||||
|
| `/primeiro-acesso` | `primeiro-acesso` | Public | ✗ |
|
||||||
|
| `/credenciamento` | `credenciamento` | Public | ✗ |
|
||||||
|
| `/servicos` | `servicos` | Public | ✗ |
|
||||||
|
| `/servicos/certidao` | `certidao` | Public | ✗ |
|
||||||
|
| `/servicos/iptu` | `iptu` | Public | ✗ |
|
||||||
|
| `/portal/painel` | `painel` | Portal | ✓ |
|
||||||
|
| `/portal/debitos` | `debitos` | Portal | ✓ |
|
||||||
|
| `/portal/certidoes` | `certidoes-portal` | Portal | ✓ |
|
||||||
|
| `/portal/alvaras` | `alvaras` | Portal | ✓ |
|
||||||
|
| `/portal/pagamentos` | `pagamentos` | Portal | ✓ |
|
||||||
|
| `/portal/dados` | `dados` | Portal | ✓ |
|
||||||
|
|
||||||
|
Guard no router: rotas `requiresAuth: true` redirecionam para `home` se não autenticado.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Fluxo de Login (2 etapas)
|
||||||
|
|
||||||
|
```
|
||||||
|
Home (captura CPF/CNPJ)
|
||||||
|
└── router.push({ name: 'login', query: { doc: documento } })
|
||||||
|
└── LoginView.vue exibe o doc mascarado + campo senha
|
||||||
|
└── entrar() → Keycloak PKCE (a implementar em authService.js)
|
||||||
|
└── authStore.setSession(token, userInfo)
|
||||||
|
└── router.push({ name: 'painel' })
|
||||||
|
```
|
||||||
|
|
||||||
|
O documento trafega como query param (`?doc=01234567890`) — apenas os dígitos,
|
||||||
|
sem formatação. `LoginView` faz a máscara localmente.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Variáveis de Ambiente
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# .env.development.local (GIT IGNORADO — crie localmente)
|
||||||
|
VITE_API_URL=https://sistema.modumfiscal.com.br
|
||||||
|
VITE_KEYCLOAK_URL=https://keycloakprod.modumfiscal.com.br
|
||||||
|
VITE_KEYCLOAK_REALM=modumfiscal-dev
|
||||||
|
VITE_KEYCLOAK_CLIENT_ID=portal-modumfiscal-web
|
||||||
|
```
|
||||||
|
|
||||||
|
Os arquivos `.env.development` e `.env.production` estão no repo mas vazios —
|
||||||
|
servem de template. Os valores reais ficam apenas nos arquivos `.local`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Regras Críticas de Código
|
||||||
|
|
||||||
|
- **`<script setup>` obrigatório** — Options API proibida (exceto `prefeituraStore` que usa Options API por compatibilidade com o padrão do core)
|
||||||
|
- **PrimeVue auto-importado** — não adicionar `import Button from 'primevue/button'` manualmente
|
||||||
|
- **CSS:** TailwindCSS inline no template — `@apply` proibido — SCSS só em `assets/layout/`
|
||||||
|
- **`dark:` em toda view nova** — ver tabela acima
|
||||||
|
- **Máximo 300 linhas por componente** — extrair subcomponentes se ultrapassar
|
||||||
|
- **Nunca `apiClient` direto em componente** — sempre via Service
|
||||||
|
- **`router.push({ name: 'home' })` para "voltar"** — nunca `router.back()` em fluxos críticos (histórico inesperado)
|
||||||
|
- **Componentes PrimeVue com `dark:` manual nos wrappers** — PrimeVue já adapta seus internos, mas divs externas precisam de dark: Tailwind
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Acessibilidade
|
||||||
|
|
||||||
|
Widget flutuante (`AccessibilityWidget.vue`) disponível em todas as telas via `App.vue`.
|
||||||
|
Persiste preferências no `localStorage` (chaves: `a11y-fonte`, `a11y-contraste`, `a11y-escuro`).
|
||||||
|
|
||||||
|
Classes aplicadas no `<html>`:
|
||||||
|
- `a11y-font-lg` → fonte 18px
|
||||||
|
- `a11y-font-xl` → fonte 20px
|
||||||
|
- `a11y-contrast` → `filter: contrast(1.45)`
|
||||||
|
- `app-dark` → dark mode (PrimeVue + Tailwind)
|
||||||
|
|
||||||
|
ARIA implementada: skip link, `aria-label`, `aria-current`, `role="alert"`, `aria-live`,
|
||||||
|
`aria-hidden` em ícones decorativos, `role="contentinfo"` no footer.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Como Adicionar Nova Tela Pública
|
||||||
|
|
||||||
|
1. Criar `src/views/servicos/MinhaView.vue`
|
||||||
|
2. Adicionar rota em `src/router/index.js` dentro do bloco `PublicLayout`
|
||||||
|
3. Incluir botão voltar (`router.push({ name: 'home' })`) no topo
|
||||||
|
4. Adicionar `dark:` em todos os utilitários Tailwind
|
||||||
|
5. (Opcional) Adicionar no `servicosPublicos` de `HomeView.vue`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Como Adicionar Nova Tela Autenticada
|
||||||
|
|
||||||
|
1. Criar `src/views/portal/MinhaView.vue`
|
||||||
|
2. Adicionar rota em `src/router/index.js` dentro do bloco `PortalLayout` com `meta: { requiresAuth: true }` já herdado do pai
|
||||||
|
3. Criar serviço em `src/services/` que use `apiClient` (nunca `apiClientPublico`)
|
||||||
|
4. Adicionar no `navItems` de `PortalLayout.vue` se entrar na navegação principal
|
||||||
|
5. Adicionar `dark:` em todos os utilitários Tailwind
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## O Que Está Pendente
|
||||||
|
|
||||||
|
| Feature | Arquivos envolvidos | Observações |
|
||||||
|
|---|---|---|
|
||||||
|
| Keycloak PKCE real | `authService.js`, `LoginView.vue` | Estrutura pronta, falta integrar `pkce-challenge` |
|
||||||
|
| Wizard Credenciamento | `CredenciamentoView.vue` | 6 etapas: dados pessoais, endereço, documentos, representante, confirmação |
|
||||||
|
| Wizard Primeiro Acesso | `PrimeiroAcessoView.vue` | 4 etapas: identificação, código, nova senha, confirmação |
|
||||||
|
| Certidão (implementação real) | `CertidaoView.vue` | Input CPF/CNPJ → POST API → download PDF |
|
||||||
|
| IPTU (implementação real) | `IptuView.vue` | Input inscrição imobiliária → GET débitos → carnê PDF |
|
||||||
|
| Endpoint avisos carousel | `HomeView.vue` | `GET /api/v1/publico/avisos/{dominio}` (dados mockados atualmente) |
|
||||||
|
| Campo `pathBackground` na API | `prefeituraBoot.js` | Backend ainda não retorna — mapeamento estático por `template` como POC |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Comandos
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run dev # dev server (http://localhost:5173)
|
||||||
|
npm run build # build produção
|
||||||
|
npm run lint # ESLint + fix
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Pitfalls Conhecidos
|
||||||
|
|
||||||
|
| Erro | Correto |
|
||||||
|
|---|---|
|
||||||
|
| `applyTemplate()` no bootstrap | Chamar no `App.vue onMounted` — PrimeVue precisa estar inicializado |
|
||||||
|
| `router.back()` no botão voltar | `router.push({ name: 'home' })` — back usa histórico do browser |
|
||||||
|
| `import Button from 'primevue/button'` | Não importar — PrimeVue é auto-importado |
|
||||||
|
| `@apply` no template | TailwindCSS inline — `@apply` só em `layout.scss` e nunca |
|
||||||
|
| Utilitário Tailwind sem `dark:` | Sempre adicionar par dark: para qualquer bg/text/border |
|
||||||
|
| `apiClient` direto na view | Criar Service e chamar pela view |
|
||||||
|
| Path relativo de logo/foto da API | `resolverUrl()` em `prefeituraBoot.js` converte para URL absoluta |
|
||||||
Loading…
x
Reference in New Issue
Block a user